Initial import of ijwb, an IntelliJ plugin for Bazel
diff --git a/.gitignore b/.gitignore
index e69de29..ac51a05 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1 @@
+bazel-*
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..877f4d1
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,9 @@
+# This the official list of Bazel authors for copyright purposes.
+# This file is distinct from the CONTRIBUTORS files.
+# See the latter for an explanation.
+
+# Names should be added to this file as:
+# Name or Organization <email address>
+# The email address is not required for organizations.
+
+Google Inc.
\ No newline at end of file
diff --git a/BUILD b/BUILD
new file mode 100644
index 0000000..ca39524
--- /dev/null
+++ b/BUILD
@@ -0,0 +1,33 @@
+#
+# Description: Blaze plugin for various IntelliJ products.
+#
+
+# IJwB tests, run with an IntelliJ plugin SDK
+test_suite(
+    name = "ijwb_tests",
+    tests = [
+        "//blaze-base:integration_tests",
+        "//blaze-base:unit_tests",
+        "//blaze-java:integration_tests",
+        "//blaze-java:unit_tests",
+        "//blaze-plugin-dev:integration_tests",
+    ],
+)
+
+# ASwB tests, run with an Android Studio plugin SDK
+test_suite(
+    name = "aswb_tests",
+    tests = [
+        "//aswb:unit_tests",
+        "//blaze-base:unit_tests",
+        "//blaze-java:unit_tests",
+    ],
+)
+
+# Version file
+filegroup(
+    name = "version",
+    srcs = ["VERSION"],
+    visibility = ["//visibility:public"],
+)
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..9d3d1aa
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,27 @@
+Want to contribute? Great! First, read this page (including the small print at the end).
+
+### Before you contribute
+**Before we can use your code, you must sign the
+[Google Individual Contributor License Agreement](https://developers.google.com/open-source/cla/individual?csw=1)
+(CLA)**, which you can do online.
+
+The CLA is necessary mainly because you own the copyright to your changes,
+even after your contribution becomes part of our codebase, so we need your
+permission to use and distribute your code. We also need to be sure of
+various other things — for instance that you'll tell us if you know that
+your code infringes on other people's patents. You don't have to sign
+the CLA until after you've submitted your code for review and a member has
+approved it, but you must do it before we can put your code into our codebase.
+
+Before you start working on a larger contribution, you should get in touch
+with us first. Use the issue tracker to explain your idea so we can help and
+possibly guide you.
+
+### Code reviews and other contributions.
+**All submissions, including submissions by project members, require review.**
+Please follow the instructions in [the contributors documentation](http://bazel.io/contributing.html).
+
+### The small print
+Contributions made by corporations are covered by a different agreement than
+the one above, the
+[Software Grant and Corporate Contributor License Agreement](https://cla.developers.google.com/about/google-corporate).
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
new file mode 100644
index 0000000..a5b7324
--- /dev/null
+++ b/CONTRIBUTORS
@@ -0,0 +1,12 @@
+# People who have agreed to one of the CLAs and can contribute patches.
+# The AUTHORS file lists the copyright holders; this file
+# lists people.  For example, Google employees are listed here
+# but not in AUTHORS, because Google holds the copyright.
+#
+# https://developers.google.com/open-source/cla/individual
+# https://developers.google.com/open-source/cla/corporate
+#
+# Names should be added to this file as:
+#     Name <email address>
+
+Brendan Douglas <brendandouglas@google.com>
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d4b039e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# An IntelliJ plugin for [Bazel](http://bazel.io) projects
+
+## Installation
+
+You can find our plugin in the Jetbrains plugin repository by going to
+IntelliJ -> Settings -> Browse Repositories, and searching for 'IntelliJ with Bazel'.
+
+## Usage
+
+To import an existing Bazel project, choose 'Import Bazel Project',
+and follow the instructions in the project import wizard.
+
+## Building the plugin
+
+Install Bazel, then run 'bazel build //ijwb:ijwb_bazel' from
+the project root. This will create a plugin jar in
+'bazel-genfiles/ijwb/ijwb_bazel.jar'.
\ No newline at end of file
diff --git a/VERSION b/VERSION
new file mode 100644
index 0000000..2b26b8d
--- /dev/null
+++ b/VERSION
@@ -0,0 +1 @@
+1.5.9
diff --git a/WORKSPACE b/WORKSPACE
new file mode 100644
index 0000000..e9e2b96
--- /dev/null
+++ b/WORKSPACE
@@ -0,0 +1,28 @@
+workspace(name = "intellij_with_bazel")
+
+# The plugin api for IntelliJ 2016.1.3. This is required to build IJwB,
+# and run integration tests.
+new_http_archive(
+    name = "intellij_latest",
+    build_file = "remote_platform_sdks/BUILD.idea",
+    sha256 = "d1cd3f9fd650c00ba85181da6d66b4b80b8e48ce5f4f15b5f4dc67453e96a179",
+    url = "https://download.jetbrains.com/idea/ideaIC-2016.1.3.tar.gz",
+)
+
+# The plugin api for CLion 2016.1.3. This is required to build CLwB,
+# and run integration tests.
+new_http_archive(
+    name = "clion_latest",
+    build_file = "remote_platform_sdks/BUILD.clion",
+    sha256 = "470063f1bb65ba03c6e1aba354cb81e2c04bd280d9b8da98622be1ba6b0a9c88",
+    url = "https://download.jetbrains.com/cpp/CLion-2016.1.3.tar.gz",
+)
+
+# The plugin api for Android Studio 2.2. preview 4. This is required to build ASwB,
+# and run integration tests.
+new_http_archive(
+    name = "android_studio_latest",
+    build_file = "remote_platform_sdks/BUILD.android_studio",
+    sha256 = "530b630914b42f9ad9f5442a36b421214838443429a4a1b96194d45a5d586f17",
+    url = "https://dl.google.com/dl/android/studio/ide-zips/2.2.0.3/android-studio-ide-145.3001415-linux.zip",
+)
diff --git a/aswb/.bazelproject b/aswb/.bazelproject
new file mode 100644
index 0000000..3cbf8dc
--- /dev/null
+++ b/aswb/.bazelproject
@@ -0,0 +1,19 @@
+directories:
+  .
+  -ijwb
+  -blaze-plugin-dev
+  -clwb
+  -blaze-cpp
+
+targets:
+  //aswb:aswb_bazel
+  //:aswb_tests
+
+workspace_type: intellij_plugin
+
+build_flags:
+  --define=ij_product=android-studio-latest
+
+test_sources:
+  */tests/unittests*
+  */tests/integrationtests*
diff --git a/aswb/BUILD b/aswb/BUILD
new file mode 100644
index 0000000..01df2a6
--- /dev/null
+++ b/aswb/BUILD
@@ -0,0 +1,83 @@
+#
+# Description: Builds ASwB for blaze and bazel
+#
+
+load(
+    "//build_defs:build_defs.bzl",
+    "merged_plugin_xml",
+    "stamped_plugin_xml",
+    "intellij_plugin",
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml_common",
+    srcs = [
+        "src/META-INF/aswb.xml",
+        "//blaze-base:plugin_xml",
+        "//blaze-cpp:plugin_xml",
+        "//blaze-java:plugin_xml",
+    ],
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml_bazel",
+    srcs = [
+        "src/META-INF/aswb_bazel.xml",
+        ":merged_plugin_xml_common",
+    ],
+)
+
+stamped_plugin_xml(
+    name = "stamped_plugin_xml_bazel",
+    include_product_code_in_stamp = True,
+    plugin_xml = ":merged_plugin_xml_bazel",
+    stamp_since_build = True,
+    version_file = "//:version",
+)
+
+java_library(
+    name = "aswb_lib",
+    srcs = glob(["src/**/*.java"]),
+    resources = glob(["resources/**/*"]),
+    visibility = ["//visibility:public"],
+    deps = [
+        "//blaze-base",
+        "//blaze-base:proto-deps",
+        "//blaze-cpp",
+        "//blaze-java",
+        "//intellij-platform-sdk:bundled_plugins",
+        "//intellij-platform-sdk:plugin_api",
+        "//third_party:jsr305",
+    ],
+)
+
+load(
+    "//intellij_test:test_defs.bzl",
+    "intellij_test",
+)
+
+intellij_test(
+    name = "unit_tests",
+    srcs = glob(["tests/unittests/**/*.java"]),
+    test_package_root = "com.google.idea.blaze.android",
+    deps = [
+        ":aswb_lib",
+        "//blaze-base",
+        "//blaze-base:proto-deps",
+        "//blaze-base:unit_test_utils",
+        "//blaze-java",
+        "//intellij-platform-sdk:bundled_plugins_for_tests",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//intellij_test:lib",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
+
+intellij_plugin(
+    name = "aswb_bazel",
+    plugin_xml = ":stamped_plugin_xml_bazel",
+    deps = [
+        ":aswb_lib",
+    ],
+)
diff --git a/aswb/resources/icons/crow.png b/aswb/resources/icons/crow.png
new file mode 100644
index 0000000..ce2de56
--- /dev/null
+++ b/aswb/resources/icons/crow.png
Binary files differ
diff --git a/aswb/resources/icons/crow@2x.png b/aswb/resources/icons/crow@2x.png
new file mode 100644
index 0000000..77294e8
--- /dev/null
+++ b/aswb/resources/icons/crow@2x.png
Binary files differ
diff --git a/aswb/resources/icons/crowToolWindow.png b/aswb/resources/icons/crowToolWindow.png
new file mode 100644
index 0000000..f5be2d0
--- /dev/null
+++ b/aswb/resources/icons/crowToolWindow.png
Binary files differ
diff --git a/aswb/resources/icons/mobileInstallDebug.png b/aswb/resources/icons/mobileInstallDebug.png
new file mode 100644
index 0000000..591c6bf
--- /dev/null
+++ b/aswb/resources/icons/mobileInstallDebug.png
Binary files differ
diff --git a/aswb/resources/icons/mobileInstallDebug@2x.png b/aswb/resources/icons/mobileInstallDebug@2x.png
new file mode 100644
index 0000000..fe2de54
--- /dev/null
+++ b/aswb/resources/icons/mobileInstallDebug@2x.png
Binary files differ
diff --git a/aswb/resources/icons/mobileInstallRun.png b/aswb/resources/icons/mobileInstallRun.png
new file mode 100644
index 0000000..b0a3a57
--- /dev/null
+++ b/aswb/resources/icons/mobileInstallRun.png
Binary files differ
diff --git a/aswb/resources/icons/mobileInstallRun@2x.png b/aswb/resources/icons/mobileInstallRun@2x.png
new file mode 100644
index 0000000..4033931
--- /dev/null
+++ b/aswb/resources/icons/mobileInstallRun@2x.png
Binary files differ
diff --git a/aswb/src/META-INF/aswb.xml b/aswb/src/META-INF/aswb.xml
new file mode 100644
index 0000000..87eb089
--- /dev/null
+++ b/aswb/src/META-INF/aswb.xml
@@ -0,0 +1,91 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+  <id>com.google.idea.blaze.aswb</id>
+  <vendor>Google</vendor>
+
+  <depends optional="true">com.intellij.modules.androidstudio</depends>
+  <depends>org.jetbrains.android</depends>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <java.elementFinder implementation="com.google.idea.blaze.android.resources.AndroidResourceClassFinder"
+        order="first, before java"/>
+    <java.elementFinder implementation="com.google.idea.blaze.android.resources.AndroidResourcePackageFinder"/>
+    <stepsBeforeRunProvider implementation="com.google.idea.blaze.android.run.BlazeBeforeRunTaskProvider"/>
+    <projectService serviceImplementation="com.google.idea.blaze.android.resources.LightResourceClassService"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestClassRunConfigurationProducer"
+        order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestMethodRunConfigurationProducer"
+        order="first"/>
+    <configurationType implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationType"/>
+    <configurationType implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestRunConfigurationType"/>
+    <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"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="org.jetbrains.android.actions">
+    <newResourceCreationHandler
+        implementation="com.google.idea.blaze.android.resources.actions.BlazeNewResourceCreationHandler" />
+  </extensions>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.android.sync.BlazeAndroidSyncPlugin"/>
+    <SyncListener implementation="com.google.idea.blaze.android.sync.BlazeAndroidSyncListener"/>
+    <SyncListener implementation="com.google.idea.blaze.android.cppimpl.BlazeNdkSupportEnabler"/>
+    <SyncListener implementation="com.google.idea.blaze.android.manifest.ManifestParser$ClearManifestParser"/>
+    <RuleConfigurationFactory implementation="com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationType$BlazeAndroidBinaryRuleConfigurationFactory"/>
+    <RuleConfigurationFactory implementation="com.google.idea.blaze.android.run.test.BlazeAndroidTestRunConfigurationType$BlazeAndroidTestRuleConfigurationFactory"/>
+    <java.JavaSyncAugmenter implementation="com.google.idea.blaze.android.sync.BlazeAndroidJavaSyncAugmenter"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.android.ide">
+    <sdkEventListener implementation="com.google.idea.blaze.android.sdk.AndroidSdkListener"/>
+  </extensions>
+
+  <extensionPoints>
+    <extensionPoint qualifiedName="com.google.idea.blaze.android.InstrumentationRunnerProvider"
+                    interface="com.google.idea.blaze.android.run.test.InstrumentationRunnerProvider"/>
+  </extensionPoints>
+
+  <!-- BEGIN NDK SUPPORT -->
+  <extensions defaultExtensionNs="com.intellij">
+    <applicationService serviceInterface="com.google.idea.blaze.android.cppapi.BlazeNativeDebuggerIdProvider"
+                        serviceImplementation="com.google.idea.blaze.android.cppimpl.debug.BlazeNativeAndroidDebuggerIdProviderImpl"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="cidr.debugger">
+    <languageSupportFactory implementation="com.google.idea.blaze.android.cppimpl.debug.BlazeAndroidNativeDebuggerLanguageSupportFactory"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.android.run">
+    <androidDebugger implementation="com.google.idea.blaze.android.cppimpl.debug.BlazeAutoAndroidDebugger"/>
+  </extensions>
+  <!-- END NDK SUPPORT -->
+
+  <application-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.android.plugin.PluginCompatibilityEnforcer</implementation-class>
+    </component>
+  </application-components>
+
+</idea-plugin>
\ No newline at end of file
diff --git a/aswb/src/META-INF/aswb_bazel.xml b/aswb/src/META-INF/aswb_bazel.xml
new file mode 100644
index 0000000..abcc021
--- /dev/null
+++ b/aswb/src/META-INF/aswb_bazel.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+  <name>Android Studio with Bazel</name>
+  <description>Provides the ability to import Bazel projects in Android Studio.</description>
+</idea-plugin>
diff --git a/aswb/src/META-INF/aswb_blaze.xml b/aswb/src/META-INF/aswb_blaze.xml
new file mode 100644
index 0000000..0e3acf7
--- /dev/null
+++ b/aswb/src/META-INF/aswb_blaze.xml
@@ -0,0 +1,19 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+  <name>Android Studio with Blaze</name>
+  <description>Provides the ability to import Blaze projects in Android Studio.</description>
+</idea-plugin>
diff --git a/aswb/src/com/google/idea/blaze/android/cppapi/BlazeNativeDebuggerIdProvider.java b/aswb/src/com/google/idea/blaze/android/cppapi/BlazeNativeDebuggerIdProvider.java
new file mode 100644
index 0000000..4ecc40d
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/cppapi/BlazeNativeDebuggerIdProvider.java
@@ -0,0 +1,29 @@
+/*
+ * 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.cppapi;
+
+import com.intellij.openapi.components.ServiceManager;
+
+import javax.annotation.Nullable;
+
+public abstract class BlazeNativeDebuggerIdProvider {
+  @Nullable
+  public static BlazeNativeDebuggerIdProvider getInstance() {
+    return ServiceManager.getService(BlazeNativeDebuggerIdProvider.class);
+  }
+
+  public abstract String getDebuggerId();
+}
diff --git a/aswb/src/com/google/idea/blaze/android/cppapi/NdkSupport.java b/aswb/src/com/google/idea/blaze/android/cppapi/NdkSupport.java
new file mode 100644
index 0000000..8435113
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/cppapi/NdkSupport.java
@@ -0,0 +1,22 @@
+/*
+ * 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.cppapi;
+
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+
+public class NdkSupport {
+  public static final BoolExperiment NDK_SUPPORT = new BoolExperiment("ndk.support", false);
+}
diff --git a/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
new file mode 100644
index 0000000..a162635
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/BlazeNdkSupportEnabler.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.cppimpl;
+
+import com.android.tools.ndk.NdkHelper;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.blaze.cpp.BlazeCWorkspace;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import org.jetbrains.annotations.NotNull;
+
+import static com.jetbrains.cidr.lang.OCLanguage.LANGUAGE_SUPPORT_DISABLED;
+
+public final class BlazeNdkSupportEnabler implements SyncListener {
+  @Override
+  public void onSyncStart(Project project) {
+  }
+
+  @Override
+  public void onSyncComplete(Project project,
+                             BlazeImportSettings importSettings,
+                             ProjectViewSet projectViewSet,
+                             BlazeProjectData blazeProjectData) {
+    boolean enabled = blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.C);
+    enableCSupportInIde(project, enabled);
+  }
+
+  /**
+   * If {@code enabled} is true, this method will enable C support in the IDE if it is not already enabled. if {@code enabled} is false this
+   * method will clear out any currently stored information in the IDE about C and will disable C support in the IDE, unless support is
+   * already disabled.
+   *
+   * </p>
+   * In either case, if the value of enabled matches what the IDE currently does, this method will do nothing.
+   *
+   * @param project the project to enable or disable c support in.
+   * @param enabled if true, turn on C support in the IDE. If false, turn off C support in the IDE.
+   */
+  private static void enableCSupportInIde(Project project, boolean enabled) {
+    BlazeCWorkspace workspace = BlazeCWorkspace.getInstance(project);
+    Boolean isCurrentlyEnabled = !LANGUAGE_SUPPORT_DISABLED.get(project, false);
+    if (isCurrentlyEnabled != enabled) {
+      NdkHelper.disableCppLanguageSupport(project, !enabled);
+      rebuildSymbols(project, workspace);
+    }
+  }
+
+  private static void rebuildSymbols(@NotNull Project project, @NotNull OCWorkspace workspace) {
+    ApplicationManager.getApplication().runReadAction(() -> {
+      if (project.isDisposed()) {
+        return;
+      }
+      // Notifying BuildSettingsChangeTracker in unitTestMode will leads to a dead lock. See b/23087433 for more information.
+      if (!ApplicationManager.getApplication().isUnitTestMode()) {
+        workspace.getModificationTrackers().getBuildSettingsChangesTracker().incModificationCount();
+      }
+    });
+  }
+
+  @Override
+  public void afterSync(Project project, boolean successful) {
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeAndroidNativeDebuggerLanguageSupportFactory.java b/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeAndroidNativeDebuggerLanguageSupportFactory.java
new file mode 100644
index 0000000..9ca88db
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeAndroidNativeDebuggerLanguageSupportFactory.java
@@ -0,0 +1,90 @@
+/*
+ * 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.cppimpl.debug;
+
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfiguration;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.openapi.application.Result;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.testFramework.LightVirtualFile;
+import com.intellij.xdebugger.XSourcePosition;
+import com.intellij.xdebugger.evaluation.EvaluationMode;
+import com.intellij.xdebugger.evaluation.XDebuggerEditorsProvider;
+import com.jetbrains.cidr.execution.debugger.OCDebuggerLanguageSupportFactory;
+import com.jetbrains.cidr.execution.debugger.OCDebuggerTypesHelper;
+import com.jetbrains.cidr.lang.OCFileType;
+import com.jetbrains.cidr.lang.OCLanguage;
+import com.jetbrains.cidr.lang.util.OCElementFactory;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+
+public class BlazeAndroidNativeDebuggerLanguageSupportFactory extends OCDebuggerLanguageSupportFactory {
+  @Override
+  public XDebuggerEditorsProvider createEditor(RunProfile profile) {
+    if (profile == null) {
+      return new DebuggerEditorsProvider();
+    }
+    if (profile instanceof BlazeAndroidRunConfiguration) {
+      BlazeAndroidRunConfiguration runConfig = (BlazeAndroidRunConfiguration)profile;
+      if (runConfig.getCommonState().isNativeDebuggingEnabled()) {
+        return new DebuggerEditorsProvider();
+      }
+    }
+    return null;
+  }
+
+  private static class DebuggerEditorsProvider extends XDebuggerEditorsProvider {
+    @NotNull
+    @Override
+    public FileType getFileType() {
+      return OCFileType.INSTANCE;
+    }
+
+    @NotNull
+    @Override
+    public Document createDocument(final Project project,
+                                   final String text,
+                                   @Nullable XSourcePosition sourcePosition,
+                                   final EvaluationMode mode) {
+      final PsiElement context = OCDebuggerTypesHelper.getContextElement(sourcePosition, project);
+      if (context != null && context.getLanguage() == OCLanguage.getInstance())   {
+        return new WriteAction<Document>() {
+          @Override
+          protected void run(Result<Document> result) throws Throwable {
+            PsiFile fragment = mode == EvaluationMode.EXPRESSION
+                               ? OCElementFactory.expressionCodeFragment(text, project, context, true, false)
+                               : OCElementFactory.expressionOrStatementsCodeFragment(text, project, context, true, false);
+            //noinspection ConstantConditions
+            result.setResult(PsiDocumentManager.getInstance(project).getDocument(fragment));
+          }
+        }.execute().getResultObject();
+      }
+      else {
+        final LightVirtualFile plainTextFile = new LightVirtualFile("oc-debug-editor-when-no-source-position-available.txt", text);
+        //noinspection ConstantConditions
+        return FileDocumentManager.getInstance().getDocument(plainTextFile);
+      }
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeAutoAndroidDebugger.java b/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeAutoAndroidDebugger.java
new file mode 100644
index 0000000..2434eb9
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeAutoAndroidDebugger.java
@@ -0,0 +1,39 @@
+/*
+ * 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.cppimpl.debug;
+
+import com.android.tools.ndk.run.editor.AutoAndroidDebugger;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+public class BlazeAutoAndroidDebugger extends AutoAndroidDebugger {
+  public static String ID = "BlazeAuto";
+
+  @Override
+  protected boolean isNativeProject(@NotNull Project project) {
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    return blazeProjectData != null && blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.C);
+  }
+
+  @NotNull
+  @Override
+  public String getId() {
+    return ID;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeNativeAndroidDebuggerIdProviderImpl.java b/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeNativeAndroidDebuggerIdProviderImpl.java
new file mode 100644
index 0000000..c87b3ac
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/cppimpl/debug/BlazeNativeAndroidDebuggerIdProviderImpl.java
@@ -0,0 +1,26 @@
+/*
+ * 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.cppimpl.debug;
+
+import com.google.idea.blaze.android.cppapi.BlazeNativeDebuggerIdProvider;
+
+public class BlazeNativeAndroidDebuggerIdProviderImpl extends BlazeNativeDebuggerIdProvider {
+
+  @Override
+  public String getDebuggerId() {
+    return BlazeAutoAndroidDebugger.ID;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java b/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
new file mode 100644
index 0000000..cae3b0f
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/manifest/ManifestParser.java
@@ -0,0 +1,107 @@
+/*
+ * 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.manifest;
+
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.ModalityState;
+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.psi.PsiDocumentManager;
+import com.intellij.util.ArrayUtil;
+import org.jetbrains.android.dom.manifest.Manifest;
+import org.jetbrains.android.util.AndroidUtils;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+/**
+ * Parses manifests from the project.
+ */
+public class ManifestParser {
+  private static final Logger LOG = Logger.getInstance(ManifestParser.class);
+  private final Project project;
+  private Map<File, Manifest> manifestFileMap = Maps.newHashMap();
+
+  public static ManifestParser getInstance(Project project) {
+    return ServiceManager.getService(project, ManifestParser.class);
+  }
+
+  public ManifestParser(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  public Manifest getManifest(File file) {
+    if (!file.exists()) {
+      return null;
+    }
+    Manifest manifest = manifestFileMap.get(file);
+    if (manifest != null) {
+      return manifest;
+    }
+    final VirtualFile virtualFile;
+    if (ApplicationManager.getApplication().isDispatchThread()) {
+       virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file);
+    } else {
+       virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file);
+    }
+    if (virtualFile == null) {
+      LOG.error("Could not find manifest: " + file);
+      return null;
+    }
+    manifest = AndroidUtils.loadDomElement(project, virtualFile, Manifest.class);
+    manifestFileMap.put(file, manifest);
+    return manifest;
+  }
+
+  public void refreshManifests(Collection<File> manifestFiles) {
+    List<VirtualFile> manifestVirtualFiles = manifestFiles.stream()
+      .map(file -> VfsUtil.findFileByIoFile(file, false))
+      .filter(Objects::nonNull)
+      .collect(Collectors.toList());
+
+    VfsUtil.markDirtyAndRefresh(false, false, false, ArrayUtil.toObjectArray(manifestVirtualFiles, VirtualFile.class));
+    ApplicationManager.getApplication().invokeAndWait(
+      () -> PsiDocumentManager.getInstance(project).commitAllDocuments(),
+      ModalityState.any()
+    );
+  }
+
+  public static class ClearManifestParser extends SyncListener.Adapter {
+    @Override
+    public void onSyncComplete(Project project,
+                               BlazeImportSettings importSettings,
+                               ProjectViewSet projectViewSet,
+                               BlazeProjectData blazeProjectData) {
+      getInstance(project).manifestFileMap.clear();
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/plugin/AswbPlugin.java b/aswb/src/com/google/idea/blaze/android/plugin/AswbPlugin.java
new file mode 100644
index 0000000..a9e5866
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/plugin/AswbPlugin.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.plugin;
+
+import com.google.idea.blaze.base.plugin.BlazePluginId;
+
+/**
+ * ASwB plugin configuration information.
+ */
+public class AswbPlugin implements BlazePluginId {
+  private static final String PLUGIN_ID = "com.google.idea.blaze.aswb"; // Please keep up-to-date with plugin.xml
+
+  @Override
+  public String getPluginId() {
+    return PLUGIN_ID;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/plugin/PluginCompatibilityEnforcer.java b/aswb/src/com/google/idea/blaze/android/plugin/PluginCompatibilityEnforcer.java
new file mode 100644
index 0000000..a4a62d7
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/plugin/PluginCompatibilityEnforcer.java
@@ -0,0 +1,110 @@
+/*
+ * 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.plugin;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.io.CharStreams;
+import com.intellij.notification.NotificationDisplayType;
+import com.intellij.notification.NotificationGroup;
+import com.intellij.openapi.application.ApplicationInfo;
+import com.intellij.openapi.components.ApplicationComponent;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.ui.MessageType;
+import com.intellij.openapi.util.BuildNumber;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * Checks META-INF/product-build.txt for a product build number and compares
+ * them against the build. If incompatible, it informs the user.
+ */
+public class PluginCompatibilityEnforcer implements ApplicationComponent {
+  private static final Logger LOG = Logger.getInstance(PluginCompatibilityEnforcer.class);
+  private static final NotificationGroup NOTIFICATION_GROUP =
+    new NotificationGroup("ASwB Plugin Version", NotificationDisplayType.BALLOON, true);
+
+  public void checkPluginCompatibility() {
+    String pluginProductBuildString = readProductBuildTxt();
+    if (Strings.isNullOrEmpty(pluginProductBuildString)) {
+      return;
+    }
+    // Dev mode?
+    if (pluginProductBuildString.equals("PRODUCT_BUILD")) {
+      return;
+    }
+    BuildNumber pluginProductBuild = BuildNumber.fromString(pluginProductBuildString);
+    if (pluginProductBuild == null) {
+      LOG.warn("Invalid META-INF/product-build.txt");
+      return;
+    }
+
+    if (!isCompatible(pluginProductBuild)) {
+      String message = Joiner.on(' ').join(
+        "Invalid Android Studio version for the ASwB plugin.",
+        "Android Studio version: " + ApplicationInfo.getInstance().getBuild(),
+        "Compatible version: " + pluginProductBuild,
+        "Please update the ASwB plugin from the plugin manager."
+      );
+      NOTIFICATION_GROUP.createNotification(message, MessageType.ERROR).notify(null);
+      LOG.warn(message);
+    }
+  }
+
+  private boolean isCompatible(BuildNumber pluginProductBuild) {
+    if (pluginProductBuild.isSnapshot()) {
+      return true;
+    }
+    BuildNumber buildNumber = ApplicationInfo.getInstance().getBuild();
+    if (buildNumber == null || buildNumber.isSnapshot()) {
+      return true;
+    }
+    return buildNumber.equals(pluginProductBuild);
+  }
+
+  @Nullable
+  private String readProductBuildTxt() {
+    try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("META-INF/product-build.txt")) {
+      if (inputStream == null) {
+        return null;
+      }
+      return CharStreams.toString(new InputStreamReader(inputStream)).trim();
+    } catch (IOException e) {
+      LOG.error("Could not read META-INF/product-build.txt", e);
+      return null;
+    }
+  }
+
+  @Override
+  public void initComponent() {
+    checkPluginCompatibility();
+  }
+
+  @Override
+  public void disposeComponent() {
+
+  }
+
+  @NotNull
+  @Override
+  public String getComponentName() {
+    return "ASwB plugin compatibility enforcer";
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
new file mode 100644
index 0000000..1cec804
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/projectview/AndroidSdkPlatformSection.java
@@ -0,0 +1,58 @@
+/*
+ * 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.CharMatcher;
+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 org.jetbrains.annotations.Nullable;
+
+/**
+ * Allows manual override of the android sdk.
+ */
+public class AndroidSdkPlatformSection {
+  public static final SectionKey<String, ScalarSection<String>> KEY = SectionKey.of("android_sdk_platform");
+  public static final SectionParser PARSER = new AndroidSdkPlatformParser();
+
+  private static class AndroidSdkPlatformParser extends ScalarSectionParser<String> {
+    public AndroidSdkPlatformParser() {
+      super(KEY, ':');
+    }
+
+    @Nullable
+    @Override
+    protected String parseItem(
+      ProjectViewParser parser,
+      ParseContext parseContext,
+      String rest) {
+      return CharMatcher.is('\"').trimFrom(rest.trim());
+    }
+
+    @Override
+    protected void printItem(StringBuilder sb, String value) {
+      sb.append(value);
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/AndroidPackageRClass.java b/aswb/src/com/google/idea/blaze/android/resources/AndroidPackageRClass.java
new file mode 100644
index 0000000..f875247
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/AndroidPackageRClass.java
@@ -0,0 +1,158 @@
+/*
+ * 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.resources;
+
+import com.android.resources.ResourceType;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.intellij.ide.highlighter.JavaFileType;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleUtilCore;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileFactory;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.util.CachedValue;
+import com.intellij.psi.util.CachedValueProvider;
+import com.intellij.psi.util.CachedValuesManager;
+import com.intellij.psi.util.PsiModificationTracker;
+import org.jetbrains.android.augment.AndroidLightClassBase;
+import org.jetbrains.android.augment.ResourceTypeClass;
+import org.jetbrains.android.dom.converters.ResourceReferenceConverter;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Represents a dynamic "class R" for resources in an Android module.
+ */
+public class AndroidPackageRClass extends AndroidLightClassBase {
+  private static final Logger LOG = Logger.getInstance(AndroidPackageRClass.class);
+  private static final BoolExperiment USE_OUT_OF_CODE_MOD_COUNT = new BoolExperiment("use.out.of.code.modcount.for.r.class.cache", true);
+
+  @NotNull
+  private final PsiFile myFile;
+  @NotNull
+  private final String myFullyQualifiedName;
+  @NotNull
+  private final Module myModule;
+
+  private CachedValue<PsiClass[]> myClassCache;
+
+  public AndroidPackageRClass(@NotNull PsiManager psiManager,
+                              @NotNull String packageName,
+                              @NotNull Module module) {
+    super(psiManager);
+
+    myModule = module;
+    myFullyQualifiedName = packageName + AndroidResourceClassFinder.INTERNAL_R_CLASS_SHORTNAME;
+    myFile = PsiFileFactory.getInstance(myManager.getProject())
+      .createFileFromText("R.java", JavaFileType.INSTANCE, "package " + packageName + ";");
+
+    this.putUserData(ModuleUtilCore.KEY_MODULE, module);
+    // Some scenarios move up to the file level and then attempt to get the module from the file.
+    myFile.putUserData(ModuleUtilCore.KEY_MODULE, module);
+  }
+
+  @NotNull
+  public Module getModule() {
+    return myModule;
+  }
+
+  @Override
+  public String toString() {
+    return "AndroidPackageRClass";
+  }
+
+  @Nullable
+  @Override
+  public String getQualifiedName() {
+    return myFullyQualifiedName;
+  }
+
+  @Override
+  public String getName() {
+    return "R";
+  }
+
+  @Nullable
+  @Override
+  public PsiClass getContainingClass() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public PsiFile getContainingFile() {
+    return myFile;
+  }
+
+  @NotNull
+  @Override
+  public PsiClass[] getInnerClasses() {
+    if (myClassCache == null) {
+      myClassCache = CachedValuesManager.getManager(getProject())
+        .createCachedValue(new CachedValueProvider<PsiClass[]>() {
+          @Override
+          public Result<PsiClass[]> compute() {
+            return Result.create(doGetInnerClasses(),
+                                 USE_OUT_OF_CODE_MOD_COUNT.getValue() ?
+                                 PsiModificationTracker.OUT_OF_CODE_BLOCK_MODIFICATION_COUNT :
+                                 PsiModificationTracker.MODIFICATION_COUNT);
+          }
+        });
+    }
+    return myClassCache.getValue();
+  }
+
+  private PsiClass[] doGetInnerClasses() {
+    if (DumbService.isDumb(getProject())) {
+      LOG.debug("R_CLASS_AUGMENT: empty because of dumb mode");
+      return new PsiClass[0];
+    }
+
+    final AndroidFacet facet = AndroidFacet.getInstance(myModule);
+    if (facet == null) {
+      LOG.debug("R_CLASS_AUGMENT: empty because no facet");
+      return new PsiClass[0];
+    }
+
+    final Set<ResourceType> types = ResourceReferenceConverter.getResourceTypesInCurrentModule(facet);
+    final List<PsiClass> result = new ArrayList<PsiClass>();
+
+    for (ResourceType type : types) {
+      result.add(new ResourceTypeClass(facet, type.getName(), this));
+    }
+    LOG.debug("R_CLASS_AUGMENT: " + result.size() + " classes added");
+    return result.toArray(new PsiClass[result.size()]);
+  }
+
+  @Override
+  public PsiClass findInnerClassByName(@NonNls String name, boolean checkBases) {
+    for (PsiClass aClass : getInnerClasses()) {
+      if (name.equals(aClass.getName())) {
+        return aClass;
+      }
+    }
+    return null;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/AndroidResourceClassFinder.java b/aswb/src/com/google/idea/blaze/android/resources/AndroidResourceClassFinder.java
new file mode 100644
index 0000000..3cc85e1
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/AndroidResourceClassFinder.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.resources;
+
+import com.google.common.base.Strings;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElementFinder;
+import com.intellij.psi.PsiPackage;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Provides dynamic Android Resource classes (class R {...} ).
+ */
+public class AndroidResourceClassFinder extends PsiElementFinder {
+  static final String INTERNAL_R_CLASS_SHORTNAME = ".R";
+  private final Project project;
+
+  public AndroidResourceClassFinder(@NotNull Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public PsiClass findClass(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
+    PsiClass[] result = findClasses(qualifiedName, scope);
+    return result.length > 0 ? result[0] : null;
+  }
+
+  @NotNull
+  @Override
+  public PsiClass[] getClasses(@NotNull PsiPackage psiPackage, @NotNull GlobalSearchScope scope) {
+    String targetPackageName = psiPackage.getQualifiedName();
+    if (Strings.isNullOrEmpty(targetPackageName)) {
+      return PsiClass.EMPTY_ARRAY;
+    }
+    return findClasses(targetPackageName + INTERNAL_R_CLASS_SHORTNAME, scope);
+  }
+
+  @NotNull
+  @Override
+  public PsiClass[] findClasses(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
+    if (!qualifiedName.endsWith(INTERNAL_R_CLASS_SHORTNAME)) {
+      return PsiClass.EMPTY_ARRAY;
+    }
+    LightResourceClassService rClassService = LightResourceClassService.getInstance(project);
+    List<PsiClass> result = rClassService.getLightRClasses(qualifiedName, scope);
+    return result.toArray(new PsiClass[result.size()]);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/AndroidResourcePackage.java b/aswb/src/com/google/idea/blaze/android/resources/AndroidResourcePackage.java
new file mode 100644
index 0000000..3cd62a1
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/AndroidResourcePackage.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.resources;
+
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.file.PsiPackageImpl;
+
+/**
+ * A generated stub PsiPackage for generated R classes.
+ */
+public class AndroidResourcePackage extends PsiPackageImpl {
+  public AndroidResourcePackage(PsiManager manager, String qualifiedName) {
+    super(manager, qualifiedName);
+  }
+
+  @Override
+  public boolean isValid() {
+    return true;
+  }
+
+  @Override
+  public boolean canNavigate() {
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "AndroidResourcePackage: " + getQualifiedName();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/AndroidResourcePackageFinder.java b/aswb/src/com/google/idea/blaze/android/resources/AndroidResourcePackageFinder.java
new file mode 100644
index 0000000..6c37015
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/AndroidResourcePackageFinder.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.android.resources;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElementFinder;
+import com.intellij.psi.PsiPackage;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Provides dynamic Android resource packages.
+ */
+public class AndroidResourcePackageFinder extends PsiElementFinder {
+
+  private final Project project;
+
+  public AndroidResourcePackageFinder(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public PsiClass findClass(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
+    return null;
+  }
+
+  @NotNull
+  @Override
+  public PsiClass[] findClasses(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
+    return PsiClass.EMPTY_ARRAY;
+  }
+
+  @Nullable
+  @Override
+  public PsiPackage findPackage(@NotNull String qualifiedName) {
+    return LightResourceClassService.getInstance(project).findRClassPackage(qualifiedName);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/LightResourceClassService.java b/aswb/src/com/google/idea/blaze/android/resources/LightResourceClassService.java
new file mode 100644
index 0000000..e8d1cdc
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/LightResourceClassService.java
@@ -0,0 +1,119 @@
+/*
+ * 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.resources;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiPackage;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A service for storing and finding light R classes.
+ */
+public class LightResourceClassService {
+
+  @NotNull
+  private Map<String, AndroidPackageRClass> rClasses = Maps.newHashMap();
+  @NotNull
+  private Map<String, PsiPackage> rClassPackages = Maps.newHashMap();
+
+  // It should be harmless to create stub resource PsiPackages which shadow any "real" PsiPackages. Based on the ordering
+  // of PsiElementFinder it would prefer the real package (PsiElementFinderImpl has 'order="first"').
+  // Put under experiment just in case we find a problem w/ other element finders.
+  private static final BoolExperiment CREATE_STUB_RESOURCE_PACKAGES = new BoolExperiment("create.stub.resource.packages", true);
+
+  public LightResourceClassService() {
+  }
+
+  public static LightResourceClassService getInstance(@NotNull Project project) {
+    return ServiceManager.getService(project, LightResourceClassService.class);
+  }
+
+  public static class Builder {
+    Map<String, AndroidPackageRClass> rClassMap = Maps.newHashMap();
+    Map<String, PsiPackage> rClassPackages = Maps.newHashMap();
+
+    private final PsiManager psiManager;
+
+    public Builder(Project project) {
+      this.psiManager = PsiManager.getInstance(project);
+    }
+
+    public void addRClass(String resourceJavaPackage, Module module) {
+      AndroidPackageRClass rClass = new AndroidPackageRClass(
+        psiManager,
+        resourceJavaPackage,
+        module
+      );
+      rClassMap.put(getQualifiedRClassName(resourceJavaPackage), rClass);
+      if (CREATE_STUB_RESOURCE_PACKAGES.getValue()) {
+        addStubPackages(resourceJavaPackage);
+      }
+    }
+
+    @NotNull
+    private static String getQualifiedRClassName(@NotNull String packageName) {
+      return packageName + ".R";
+    }
+
+    private void addStubPackages(String resourceJavaPackage) {
+      while (!resourceJavaPackage.isEmpty()) {
+        if (rClassPackages.containsKey(resourceJavaPackage)) {
+          return;
+        }
+        rClassPackages.put(resourceJavaPackage, new AndroidResourcePackage(psiManager, resourceJavaPackage));
+        int nextIndex = resourceJavaPackage.lastIndexOf('.');
+        if (nextIndex < 0) {
+          return;
+        }
+        resourceJavaPackage = resourceJavaPackage.substring(0, nextIndex);
+      }
+    }
+  }
+
+  public void installRClasses(Builder builder) {
+    this.rClasses = builder.rClassMap;
+    this.rClassPackages = builder.rClassPackages;
+  }
+
+  @NotNull
+  public List<PsiClass> getLightRClasses(
+    @NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
+    AndroidPackageRClass rClass = this.rClasses.get(qualifiedName);
+    if (rClass != null) {
+      if (scope.isSearchInModuleContent(rClass.getModule())) {
+        return ImmutableList.of(rClass);
+      }
+    }
+    return ImmutableList.of();
+  }
+
+  @Nullable
+  public PsiPackage findRClassPackage(String qualifiedName) {
+    return rClassPackages.get(qualifiedName);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceDirectoryDialog.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceDirectoryDialog.java
new file mode 100644
index 0000000..64d46f4
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceDirectoryDialog.java
@@ -0,0 +1,244 @@
+/*
+ * 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.resources.actions;
+
+import com.android.resources.ResourceFolderType;
+import com.intellij.CommonBundle;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiManager;
+import com.intellij.ui.ComboboxWithBrowseButton;
+import com.intellij.ui.EnumComboBoxModel;
+import com.intellij.ui.ListCellRendererWrapper;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.uiDesigner.core.GridConstraints;
+import com.intellij.uiDesigner.core.GridLayoutManager;
+import org.jetbrains.android.actions.CreateResourceDirectoryDialogBase;
+import org.jetbrains.android.actions.ElementCreatingValidator;
+import org.jetbrains.android.uipreview.DeviceConfiguratorPanel;
+import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Dialog to decide where to create a res/ subdirectory (e.g., layout/, values-foo/, etc.)
+ * and how to name the subdirectory based on resource type and chosen configuration.
+ */
+public class BlazeCreateResourceDirectoryDialog extends CreateResourceDirectoryDialogBase {
+
+  private JComboBox myResourceTypeComboBox;
+  private JPanel myDeviceConfiguratorWrapper;
+  private JTextField myDirectoryNameTextField;
+  private JPanel myContentPanel;
+  private JBLabel myErrorLabel;
+  private ComboboxWithBrowseButton myResDirCombo;
+  private JBLabel myResDirLabel;
+
+  private final DeviceConfiguratorPanel myDeviceConfiguratorPanel;
+  private ElementCreatingValidator myValidator;
+  private ValidatorFactory myValidatorFactory;
+  private PsiDirectory myResDirectory;
+  private DataContext myDataContext;
+
+  public BlazeCreateResourceDirectoryDialog(Project project,
+                                            @Nullable Module module,
+                                            @Nullable ResourceFolderType resType,
+                                            @Nullable PsiDirectory resDirectory,
+                                            @Nullable DataContext dataContext,
+                                            ValidatorFactory validatorFactory) {
+    super(project);
+    setupUi();
+    myResDirectory = resDirectory;
+    myDataContext = dataContext;
+    myValidatorFactory = validatorFactory;
+    myResourceTypeComboBox.setModel(new EnumComboBoxModel<>(ResourceFolderType.class));
+    myResourceTypeComboBox.setRenderer(new ListCellRendererWrapper() {
+      @Override
+      public void customize(JList list, Object value, int index, boolean selected, boolean hasFocus) {
+        if (value instanceof ResourceFolderType) {
+          setText(((ResourceFolderType)value).getName());
+        }
+      }
+    });
+
+    myDeviceConfiguratorPanel = setupDeviceConfigurationPanel(myResourceTypeComboBox, myDirectoryNameTextField, myErrorLabel);
+    myDeviceConfiguratorWrapper.add(myDeviceConfiguratorPanel, BorderLayout.CENTER);
+    myResourceTypeComboBox.addActionListener(e -> myDeviceConfiguratorPanel.applyEditors());
+
+    if (resType != null) {
+      myResourceTypeComboBox.setSelectedItem(resType);
+      myResourceTypeComboBox.setEnabled(false);
+    }
+    else {
+      // Select values by default if not otherwise specified
+      myResourceTypeComboBox.setSelectedItem(ResourceFolderType.VALUES);
+    }
+
+    // If myResDirectory is known before this, just use that.
+    myResDirLabel.setVisible(false);
+    myResDirCombo.setVisible(false);
+    if (myResDirectory == null) {
+      assert dataContext != null;
+      assert module != null;
+      // Try to figure out from context (e.g., right click in project view).
+      VirtualFile contextFile = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
+      if (contextFile != null) {
+        PsiManager manager = PsiManager.getInstance(project);
+        VirtualFile virtualDirectory = BlazeCreateResourceUtils.getResDirFromDataContext(contextFile);
+        PsiDirectory directory = virtualDirectory != null ? manager.findDirectory(virtualDirectory) : null;
+        if (directory != null) {
+          myResDirectory = directory;
+        }
+        else {
+          // As a last resort, if we have poor context, e.g., from File > New w/ a .java file open, set up the UI.
+          BlazeCreateResourceUtils.setupResDirectoryChoices(module.getProject(), contextFile, myResDirLabel, myResDirCombo);
+        }
+      }
+    }
+
+    myDeviceConfiguratorPanel.updateAll();
+    setOKActionEnabled(myDirectoryNameTextField.getText().length() > 0);
+    init();
+  }
+
+  @Override
+  protected void doOKAction() {
+    final String dirName = myDirectoryNameTextField.getText();
+    assert dirName != null;
+    PsiDirectory resourceDirectory = getResourceDirectory();
+    if (resourceDirectory == null) {
+      Module module = LangDataKeys.MODULE.getData(myDataContext);
+      Messages.showErrorDialog(AndroidBundle.message("check.resource.dir.error", module),
+                               CommonBundle.getErrorTitle());
+      // Not much the user can do, just close the dialog.
+      super.doOKAction();
+      return;
+    }
+    myValidator = myValidatorFactory.create(resourceDirectory);
+    if (myValidator.checkInput(dirName) && myValidator.canClose(dirName)) {
+      super.doOKAction();
+    }
+  }
+
+  @Override
+  protected String getDimensionServiceKey() {
+    return "BlazeCreateResourceDirectoryDialog";
+  }
+
+  @Override
+  public JComponent getPreferredFocusedComponent() {
+    if (myResourceTypeComboBox.isEnabled()) {
+      return myResourceTypeComboBox;
+    }
+    else {
+      return myDirectoryNameTextField;
+    }
+  }
+
+  @Override
+  @NotNull
+  public PsiElement[] getCreatedElements() {
+    return myValidator != null ? myValidator.getCreatedElements() : PsiElement.EMPTY_ARRAY;
+  }
+
+  @Nullable
+  private PsiDirectory getResourceDirectory() {
+    if (myResDirectory != null) {
+      return myResDirectory;
+    }
+    if (myResDirCombo.isVisible()) {
+      Module contextModule = LangDataKeys.MODULE.getData(myDataContext);
+      assert contextModule != null;
+      return BlazeCreateResourceUtils.getResDirFromUI(contextModule.getProject(), myResDirCombo);
+    }
+    return null;
+  }
+
+  @Nullable
+  @Override
+  protected JComponent createCenterPanel() {
+    return myContentPanel;
+  }
+
+  /**
+   * Initially generated by IntelliJ from a .form file.
+   */
+  private void setupUi() {
+    myContentPanel = new JPanel();
+    myContentPanel.setLayout(new GridLayoutManager(5, 2, new Insets(0, 0, 0, 0), -1, -1));
+    myContentPanel.setPreferredSize(new Dimension(800, 400));
+    myResourceTypeComboBox = new JComboBox();
+    myContentPanel.add(myResourceTypeComboBox, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                                   GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED,
+                                                                   null, null, null, 0, false));
+    final JBLabel jBLabel1 = new JBLabel();
+    jBLabel1.setText("Resource type:");
+    jBLabel1.setDisplayedMnemonic('R');
+    jBLabel1.setDisplayedMnemonicIndex(0);
+    myContentPanel.add(jBLabel1, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
+                                                     GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null,
+                                                     0, false));
+    myDeviceConfiguratorWrapper = new JPanel();
+    myDeviceConfiguratorWrapper.setLayout(new BorderLayout(0, 0));
+    myContentPanel.add(myDeviceConfiguratorWrapper,
+                       new GridConstraints(3, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH,
+                                           GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                           GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0,
+                                           false));
+    final JLabel label1 = new JLabel();
+    label1.setText("Directory name:");
+    label1.setDisplayedMnemonic('D');
+    label1.setDisplayedMnemonicIndex(0);
+    myContentPanel.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
+                                                   GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0,
+                                                   false));
+    myDirectoryNameTextField = new JTextField();
+    myDirectoryNameTextField.setEnabled(true);
+    myContentPanel.add(myDirectoryNameTextField,
+                       new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                           GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null,
+                                           new Dimension(150, -1), null, 0, false));
+    myErrorLabel = new JBLabel();
+    myContentPanel.add(myErrorLabel, new GridConstraints(4, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH,
+                                                         GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null,
+                                                         null, 0, false));
+    myResDirLabel = new JBLabel();
+    myResDirLabel.setText("Base directory:");
+    myResDirLabel.setDisplayedMnemonic('B');
+    myResDirLabel.setDisplayedMnemonicIndex(0);
+    myContentPanel.add(myResDirLabel, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
+                                                          GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null,
+                                                          null, 0, false));
+    myResDirCombo = new ComboboxWithBrowseButton();
+    myContentPanel.add(myResDirCombo, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                          GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null,
+                                                          null, 0, false));
+    jBLabel1.setLabelFor(myResourceTypeComboBox);
+    label1.setLabelFor(myDirectoryNameTextField);
+    myResDirLabel.setLabelFor(myResourceTypeComboBox);
+  }
+
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceFileDialog.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceFileDialog.java
new file mode 100644
index 0000000..a0bee66
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceFileDialog.java
@@ -0,0 +1,416 @@
+/*
+ * 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.resources.actions;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.ResourceConstants;
+import com.android.resources.ResourceFolderType;
+import com.android.tools.idea.res.ResourceNameValidator;
+import com.intellij.CommonBundle;
+import com.intellij.ide.actions.TemplateKindCombo;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiManager;
+import com.intellij.ui.ComboboxWithBrowseButton;
+import com.intellij.ui.DocumentAdapter;
+import com.intellij.ui.TextFieldWithAutoCompletion;
+import com.intellij.ui.TextFieldWithAutoCompletion.StringsCompletionProvider;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.uiDesigner.core.GridConstraints;
+import com.intellij.uiDesigner.core.GridLayoutManager;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.android.actions.CreateResourceFileDialogBase;
+import org.jetbrains.android.actions.CreateTypedResourceFileAction;
+import org.jetbrains.android.actions.ElementCreatingValidator;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.uipreview.DeviceConfiguratorPanel;
+import org.jetbrains.android.util.AndroidBundle;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import javax.swing.event.DocumentEvent;
+import java.awt.*;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Dialog to decide where and how to create a resource file of a given type
+ * (which base res/ directory, which subdirectory, and how to name the new file).
+ */
+public class BlazeCreateResourceFileDialog extends CreateResourceFileDialogBase {
+  private JTextField myFileNameField;
+  private TemplateKindCombo myResourceTypeCombo;
+  private JPanel myPanel;
+  private JLabel myUpDownHint;
+  private JLabel myResTypeLabel;
+  private JPanel myDeviceConfiguratorWrapper;
+  private JBLabel myErrorLabel;
+  private JTextField myDirectoryNameTextField;
+  private JPanel myRootElementFieldWrapper;
+  private JBLabel myRootElementLabel;
+  private JLabel myFileNameLabel;
+  private ComboboxWithBrowseButton myResDirCombo;
+  private JBLabel myResDirLabel;
+  private TextFieldWithAutoCompletion<String> myRootElementField;
+  private ElementCreatingValidator myValidator;
+  private ValidatorFactory myValidatorFactory;
+
+  private final DeviceConfiguratorPanel myDeviceConfiguratorPanel;
+  private final AndroidFacet myFacet;
+  private PsiDirectory myResDirectory;
+
+  public BlazeCreateResourceFileDialog(AndroidFacet facet,
+                                       Collection<CreateTypedResourceFileAction> actions,
+                                       ResourceFolderType folderType,
+                                       String filename,
+                                       String rootElement,
+                                       FolderConfiguration folderConfiguration,
+                                       boolean chooseFileName,
+                                       boolean chooseModule,
+                                       PsiDirectory resDirectory,
+                                       DataContext dataContext,
+                                       ValidatorFactory validatorFactory) {
+    super(facet.getModule().getProject());
+    setupUi();
+    myFacet = facet;
+    myResDirectory = resDirectory;
+    myValidatorFactory = validatorFactory;
+
+    myResTypeLabel.setLabelFor(myResourceTypeCombo);
+    myResourceTypeCombo.registerUpDownHint(myFileNameField);
+    myUpDownHint.setIcon(PlatformIcons.UP_DOWN_ARROWS);
+    String selectedTemplate = setupSubActions(actions, myResourceTypeCombo, folderType);
+
+    myDeviceConfiguratorPanel = setupDeviceConfigurationPanel(myDirectoryNameTextField, myResourceTypeCombo, myErrorLabel);
+    if (folderConfiguration != null) {
+      myDeviceConfiguratorPanel.init(folderConfiguration);
+    }
+
+    myResourceTypeCombo.getComboBox().addActionListener(e -> {
+      myDeviceConfiguratorPanel.applyEditors();
+      updateRootElementTextField();
+    });
+
+    if (folderType != null && selectedTemplate != null) {
+      final boolean v = folderType == ResourceFolderType.LAYOUT;
+      myRootElementLabel.setVisible(v);
+      myRootElementFieldWrapper.setVisible(v);
+
+      myResTypeLabel.setVisible(false);
+      myResourceTypeCombo.setVisible(false);
+      myUpDownHint.setVisible(false);
+      myResourceTypeCombo.setSelectedName(selectedTemplate);
+    } else {
+      // Select values by default if not otherwise specified
+      myResourceTypeCombo.setSelectedName(ResourceConstants.FD_RES_VALUES);
+    }
+
+    boolean validateImmediately = false;
+    if (filename != null && getNameError(filename) != null) {
+      chooseFileName = true;
+      validateImmediately = true;
+    }
+
+    if (filename != null) {
+      if (!chooseFileName) {
+        myFileNameField.setVisible(false);
+        myFileNameLabel.setVisible(false);
+      }
+      myFileNameField.setText(filename);
+    }
+
+    // Set up UI to choose the base directory if needed (use context to prune selection).
+    // There may be a resource directory already pre-selected, in which case hide the UI by default.
+    myResDirCombo.setVisible(false);
+    myResDirLabel.setVisible(false);
+    Project project = myFacet.getModule().getProject();
+    if (myResDirectory == null) {
+      // Try to figure out from context (e.g., right click in project view).
+      VirtualFile contextFile = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
+      if (contextFile != null) {
+        PsiManager manager = PsiManager.getInstance(project);
+        VirtualFile virtualDirectory = BlazeCreateResourceUtils.getResDirFromDataContext(contextFile);
+        PsiDirectory directory = virtualDirectory != null ? manager.findDirectory(virtualDirectory) : null;
+        if (directory != null) {
+          myResDirectory = directory;
+        }
+        else {
+          // As a last resort, if we have poor context, e.g., from File > New w/ a .java file open, set up the UI.
+          BlazeCreateResourceUtils.setupResDirectoryChoices(project, contextFile, myResDirLabel, myResDirCombo);
+        }
+      }
+      else {
+        // As a last resort, if we have no context, set up the UI.
+        BlazeCreateResourceUtils.setupResDirectoryChoices(project, null, myResDirLabel, myResDirCombo);
+      }
+    }
+
+    myDeviceConfiguratorPanel.updateAll();
+    myDeviceConfiguratorWrapper.add(myDeviceConfiguratorPanel, BorderLayout.CENTER);
+    updateOkAction();
+    updateRootElementTextField();
+
+    if (rootElement != null) {
+      myRootElementLabel.setVisible(false);
+      myRootElementFieldWrapper.setVisible(false);
+      myRootElementField.setText(rootElement);
+    }
+    init();
+
+    setTitle(AndroidBundle.message("new.resource.dialog.title"));
+
+    myFileNameField.getDocument().addDocumentListener(new DocumentAdapter() {
+      @Override
+      public void textChanged(DocumentEvent event) {
+        validateName();
+      }
+    });
+    myResourceTypeCombo.getComboBox().addActionListener(actionEvent -> validateName());
+    if (validateImmediately) {
+      validateName();
+    }
+  }
+
+  @Override
+  protected void updateOkAction() {
+    boolean enabled = myDirectoryNameTextField.getText().length() > 0;
+    enabled = enabled && getNameError(myFileNameField.getText()) == null;
+    setOKActionEnabled(enabled);
+  }
+
+  @Nullable
+  private String getNameError(String fileName) {
+    String typeName = myResourceTypeCombo.getSelectedName();
+    if (typeName != null) {
+      ResourceFolderType type = ResourceFolderType.getFolderType(typeName);
+      if (type != null) {
+        ResourceNameValidator validator = ResourceNameValidator.create(true, type);
+        return validator.getErrorText(fileName);
+      }
+    }
+
+    return null;
+  }
+
+  private void validateName() {
+    setErrorText(getNameError(myFileNameField.getText()));
+    updateOkAction();
+  }
+
+  private void updateRootElementTextField() {
+    final CreateTypedResourceFileAction action = getSelectedAction(myResourceTypeCombo);
+
+    if (action != null) {
+      final List<String> allowedTagNames = action.getSortedAllowedTagNames(myFacet);
+      myRootElementField = new TextFieldWithAutoCompletion<>(
+        myFacet.getModule().getProject(), new StringsCompletionProvider(allowedTagNames, null), true, null);
+      myRootElementField.setEnabled(allowedTagNames.size() > 1);
+      myRootElementField.setText(!action.isChooseTagName()
+                                 ? action.getDefaultRootTag()
+                                 : "");
+      myRootElementFieldWrapper.removeAll();
+      myRootElementFieldWrapper.add(myRootElementField, BorderLayout.CENTER);
+      myRootElementLabel.setLabelFor(myRootElementField);
+    }
+  }
+
+  @VisibleForTesting
+  @Override
+  public String getFileName() {
+    return myFileNameField.getText().trim();
+  }
+
+  @Override
+  protected void doOKAction() {
+    String fileName = myFileNameField.getText().trim();
+    final CreateTypedResourceFileAction action = getSelectedAction(myResourceTypeCombo);
+    assert action != null;
+
+    if (fileName.length() == 0) {
+      Messages.showErrorDialog(myPanel, AndroidBundle.message("file.name.not.specified.error"), CommonBundle.getErrorTitle());
+      return;
+    }
+
+    String rootElement = getRootElement();
+    if (!action.isChooseTagName() && rootElement.length() == 0) {
+      Messages.showErrorDialog(myPanel, AndroidBundle.message("root.element.not.specified.error"), CommonBundle.getErrorTitle());
+      return;
+    }
+
+    final String subdirName = getSubdirName();
+    if (subdirName.length() == 0) {
+      Messages.showErrorDialog(myPanel, AndroidBundle.message("directory.not.specified.error"), CommonBundle.getErrorTitle());
+      return;
+    }
+
+    final String errorMessage = getNameError(fileName);
+    if (errorMessage != null) {
+      Messages.showErrorDialog(myPanel, errorMessage, CommonBundle.getErrorTitle());
+      return;
+    }
+    PsiDirectory resDir = getResourceDirectory();
+    if (resDir == null) {
+      Messages.showErrorDialog(myPanel, AndroidBundle.message("check.resource.dir.error", myFacet.getModule()),
+                               CommonBundle.getErrorTitle());
+      super.doOKAction();
+      return;
+    }
+
+    myValidator = myValidatorFactory.create(resDir, subdirName, rootElement);
+    if (myValidator.checkInput(fileName) && myValidator.canClose(fileName)) {
+      super.doOKAction();
+    }
+  }
+
+  @Override
+  public PsiElement[] getCreatedElements() {
+    return myValidator != null ? myValidator.getCreatedElements() : PsiElement.EMPTY_ARRAY;
+  }
+
+  @Override
+  protected String getDimensionServiceKey() {
+    return "BlazeCreateResourceFileDialog";
+  }
+
+  @Nullable
+  private PsiDirectory getResourceDirectory() {
+    if (myResDirectory != null) {
+      return myResDirectory;
+    }
+    return BlazeCreateResourceUtils.getResDirFromUI(myFacet.getModule().getProject(), myResDirCombo);
+  }
+
+  private String getSubdirName() {
+    return myDirectoryNameTextField.getText().trim();
+  }
+
+  protected String getRootElement() {
+    return myRootElementField.getText().trim();
+  }
+
+  @Override
+  protected JComponent createCenterPanel() {
+    return myPanel;
+  }
+
+  @Override
+  public JComponent getPreferredFocusedComponent() {
+    String name = myFileNameField.getText();
+    if (name.length() == 0 || getNameError(name) != null) {
+      return myFileNameField;
+    }
+    else if (myResourceTypeCombo.isVisible()) {
+      return myResourceTypeCombo;
+    }
+    else if (myRootElementFieldWrapper.isVisible()) {
+      return myRootElementField;
+    }
+    return myDirectoryNameTextField;
+  }
+
+  /**
+   * Initially generated by IntelliJ from a .form file.
+   */
+  private void setupUi() {
+    myPanel = new JPanel();
+    myPanel.setLayout(new GridLayoutManager(7, 3, new Insets(0, 0, 0, 0), -1, -1));
+    myPanel.setPreferredSize(new Dimension(800, 400));
+    myFileNameLabel = new JLabel();
+    myFileNameLabel.setText("File name:");
+    myFileNameLabel.setDisplayedMnemonic('F');
+    myFileNameLabel.setDisplayedMnemonicIndex(0);
+    myPanel.add(myFileNameLabel,
+                new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myFileNameField = new JTextField();
+    myPanel.add(myFileNameField, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                     GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null,
+                                                     new Dimension(150, -1), null, 0, false));
+    myResTypeLabel = new JLabel();
+    myResTypeLabel.setText("Resource type:");
+    myResTypeLabel.setDisplayedMnemonic('R');
+    myResTypeLabel.setDisplayedMnemonicIndex(0);
+    myPanel.add(myResTypeLabel,
+                new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myUpDownHint = new JLabel();
+    myUpDownHint.setToolTipText("Pressing Up or Down arrows while in editor changes the kind");
+    myPanel.add(myUpDownHint,
+                new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myResourceTypeCombo = new TemplateKindCombo();
+    myPanel.add(myResourceTypeCombo, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                                         GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                         GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myDeviceConfiguratorWrapper = new JPanel();
+    myDeviceConfiguratorWrapper.setLayout(new BorderLayout(0, 0));
+    myPanel.add(myDeviceConfiguratorWrapper, new GridConstraints(5, 0, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH,
+                                                                 GridConstraints.SIZEPOLICY_CAN_SHRINK |
+                                                                 GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                                 GridConstraints.SIZEPOLICY_CAN_SHRINK |
+                                                                 GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false));
+    myErrorLabel = new JBLabel();
+    myPanel.add(myErrorLabel,
+                new GridConstraints(6, 0, 1, 3, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    final JBLabel jBLabel1 = new JBLabel();
+    jBLabel1.setText("Sub-directory:");
+    jBLabel1.setDisplayedMnemonic('Y');
+    jBLabel1.setDisplayedMnemonicIndex(12);
+    myPanel.add(jBLabel1,
+                new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myDirectoryNameTextField = new JTextField();
+    myDirectoryNameTextField.setEditable(true);
+    myDirectoryNameTextField.setEnabled(true);
+    myPanel.add(myDirectoryNameTextField, new GridConstraints(4, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                              GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null,
+                                                              new Dimension(150, -1), null, 0, false));
+    myRootElementLabel = new JBLabel();
+    myRootElementLabel.setText("Root element:");
+    myRootElementLabel.setDisplayedMnemonic('E');
+    myRootElementLabel.setDisplayedMnemonicIndex(5);
+    myPanel.add(myRootElementLabel,
+                new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myRootElementFieldWrapper = new JPanel();
+    myRootElementFieldWrapper.setLayout(new BorderLayout(0, 0));
+    myPanel.add(myRootElementFieldWrapper, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH,
+                                                               GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                               GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myResDirLabel = new JBLabel();
+    myResDirLabel.setText("Base directory:");
+    myResDirLabel.setDisplayedMnemonic('B');
+    myResDirLabel.setDisplayedMnemonicIndex(0);
+    myPanel.add(myResDirLabel,
+                new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myResDirCombo = new ComboboxWithBrowseButton();
+    myPanel.add(myResDirCombo, new GridConstraints(3, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                   GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null,
+                                                   0, false));
+    myFileNameLabel.setLabelFor(myFileNameField);
+    jBLabel1.setLabelFor(myDirectoryNameTextField);
+  }
+
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtils.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtils.java
new file mode 100644
index 0000000..03ce15c
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtils.java
@@ -0,0 +1,183 @@
+/*
+ * 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.resources.actions;
+
+import com.android.SdkConstants;
+import com.android.resources.ResourceFolderType;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiManager;
+import com.intellij.ui.ComboboxWithBrowseButton;
+import com.intellij.ui.components.JBLabel;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.io.File;
+import java.util.Set;
+
+/**
+ * Utilities for setting up create resource actions and dialogs.
+ */
+class BlazeCreateResourceUtils {
+
+  private static final String PLACEHOLDER_TEXT = "choose a res/ directory with dropdown or browse button";
+
+  static void setupResDirectoryChoices(@NotNull Project project, @Nullable VirtualFile contextFile,
+                                       @NotNull JBLabel resDirLabel,
+                                       @NotNull ComboboxWithBrowseButton resDirComboAndBrowser) {
+    resDirComboAndBrowser.addBrowseFolderListener(
+      project, FileChooserDescriptorFactory.createSingleFolderDescriptor());
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData != null) {
+      BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
+      if (syncData != null) {
+        ImmutableCollection<Label> labelsRelatedToContext = null;
+        File fileFromContext = null;
+        if (contextFile != null) {
+          fileFromContext = VfsUtilCore.virtualToIoFile(contextFile);
+          labelsRelatedToContext = SourceToRuleMap.getInstance(project).getTargetsForSourceFile(fileFromContext);
+          if (labelsRelatedToContext.isEmpty()) {
+            labelsRelatedToContext = null;
+          }
+        }
+        // Sort:
+        // - the "closest" thing to the contextFile at the top
+        // - the rest of the direct dirs, then transitive dirs of the context rules, then any known res dir in the project
+        //   as a backup, in alphabetical order.
+        Set<File> resourceDirs = Sets.newTreeSet();
+        Set<File> transitiveDirs = Sets.newTreeSet();
+        Set<File> allResDirs = Sets.newTreeSet();
+        for (AndroidResourceModule androidResourceModule : syncData.importResult.androidResourceModules) {
+          // labelsRelatedToContext should include deps, but as a first pass we only check the rules themselves
+          // for resources. If we come up empty, then have anyResDir as a backup.
+          allResDirs.addAll(androidResourceModule.transitiveResources);
+          if (labelsRelatedToContext != null && !labelsRelatedToContext.contains(androidResourceModule.label)) {
+            continue;
+          }
+          for (File resDir : androidResourceModule.resources) {
+            resourceDirs.add(resDir);
+          }
+          for (File resDir : androidResourceModule.transitiveResources) {
+            transitiveDirs.add(resDir);
+          }
+        }
+        // No need to show some directories twice.
+        transitiveDirs.removeAll(resourceDirs);
+        File closestDirToContext = null;
+        if (fileFromContext != null) {
+          closestDirToContext = findClosestDirToContext(fileFromContext.getPath(), resourceDirs);
+          closestDirToContext = closestDirToContext != null ? closestDirToContext :
+                                findClosestDirToContext(fileFromContext.getPath(), transitiveDirs);
+        }
+        JComboBox resDirCombo = resDirComboAndBrowser.getComboBox();
+        if (!resourceDirs.isEmpty() || !transitiveDirs.isEmpty()) {
+          for (File resourceDir : resourceDirs) {
+            resDirCombo.addItem(resourceDir);
+          }
+          for (File resourceDir : transitiveDirs) {
+            resDirCombo.addItem(resourceDir);
+          }
+        }
+        else {
+          for (File resourceDir : allResDirs) {
+            resDirCombo.addItem(resourceDir);
+          }
+        }
+        // Allow the user to browse and overwrite some of the entries.
+        resDirCombo.setEditable(true);
+        if (closestDirToContext != null) {
+          resDirCombo.setSelectedItem(closestDirToContext);
+        }
+        else {
+          String placeHolder = PLACEHOLDER_TEXT;
+          resDirCombo.insertItemAt(placeHolder, 0);
+          resDirCombo.setSelectedItem(placeHolder);
+        }
+        resDirComboAndBrowser.setVisible(true);
+        resDirLabel.setVisible(true);
+      }
+    }
+  }
+
+  private static File findClosestDirToContext(String contextPath, Set<File> resourceDirs) {
+    File closestDirToContext = null;
+    int curStringDistance = Integer.MAX_VALUE;
+    for (File resDir : resourceDirs) {
+      int distance = StringUtil.difference(contextPath, resDir.getPath());
+      if (distance < curStringDistance) {
+        curStringDistance = distance;
+        closestDirToContext = resDir;
+      }
+    }
+    return closestDirToContext;
+  }
+
+  static PsiDirectory getResDirFromUI(Project project, ComboboxWithBrowseButton directoryCombo) {
+    PsiManager psiManager = PsiManager.getInstance(project);
+    Object selectedItem = directoryCombo.getComboBox().getSelectedItem();
+    VirtualFile file = null;
+    if(selectedItem instanceof File) {
+      file = VfsUtil.findFileByIoFile((File)selectedItem, true);
+    } else if (selectedItem instanceof String) {
+      String selectedDir = (String)selectedItem;
+      if (!selectedDir.equals(PLACEHOLDER_TEXT)) {
+        file = VfsUtil.findFileByIoFile(new File(selectedDir), true);
+      }
+    }
+    if (file != null) {
+      return psiManager.findDirectory(file);
+    }
+    return null;
+  }
+
+   static VirtualFile getResDirFromDataContext(VirtualFile contextFile) {
+    // Check if the contextFile is somewhere in the <path>/res/resType/foo.xml hierarchy and return <path>/res/.
+    if (contextFile.isDirectory()) {
+      if (contextFile.getName().equalsIgnoreCase(SdkConstants.FD_RES)) {
+        return contextFile;
+      }
+      if (ResourceFolderType.getFolderType(contextFile.getName()) != null) {
+        VirtualFile parent = contextFile.getParent();
+        if (parent != null && parent.getName().equalsIgnoreCase(SdkConstants.FD_RES)) {
+          return parent;
+        }
+      }
+    }
+    else {
+      VirtualFile parent = contextFile.getParent();
+      if (parent != null && ResourceFolderType.getFolderType(parent.getName()) != null) {
+        // Otherwise, the contextFile is a file w/ a parent that is plausible. Recurse one level, on the parent.
+        return getResDirFromDataContext(parent);
+      }
+    }
+    // Otherwise, it may be too ambiguous to figure out (e.g., we're in a .java file).
+    return null;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateXmlResourcePanel.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateXmlResourcePanel.java
new file mode 100644
index 0000000..15b79fb
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeCreateXmlResourcePanel.java
@@ -0,0 +1,373 @@
+/*
+ * 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.resources.actions;
+
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.tools.idea.res.ResourceNameValidator;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiManager;
+import com.intellij.ui.ComboboxWithBrowseButton;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.uiDesigner.core.GridConstraints;
+import com.intellij.uiDesigner.core.GridLayoutManager;
+import org.jetbrains.android.actions.CreateXmlResourceDialog;
+import org.jetbrains.android.actions.CreateXmlResourcePanel;
+import org.jetbrains.android.actions.CreateXmlResourceSubdirPanel;
+import org.jetbrains.android.actions.CreateXmlResourceSubdirPanel.Parent;
+import org.jetbrains.android.util.AndroidResourceUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.*;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Embeddable UI for selecting how to create a new resource value (which XML file and directories to place it).
+ */
+public class BlazeCreateXmlResourcePanel implements CreateXmlResourcePanel,
+                                                    Parent {
+
+  private JPanel myPanel;
+  private JTextField myNameField;
+  private JTextField myValueField;
+  private JBLabel myValueLabel;
+  private JBLabel myNameLabel;
+  private JComboBox myFileNameCombo;
+  private JBLabel myResDirLabel;
+  private ComboboxWithBrowseButton myResDirCombo;
+  private JBLabel myFileNameLabel;
+
+  private final @NotNull Module myModule;
+  private final @NotNull ResourceType myResourceType;
+
+  private JPanel myDirectoriesPanel;
+  private JBLabel myDirectoriesLabel;
+  private CreateXmlResourceSubdirPanel mySubdirPanel;
+
+  private ResourceNameValidator myResourceNameValidator;
+  private @Nullable VirtualFile myContextFile;
+  private @Nullable VirtualFile myResDirectory;
+
+  public BlazeCreateXmlResourcePanel(@NotNull Module module,
+                                     @NotNull ResourceType resourceType,
+                                     @NotNull ResourceFolderType folderType,
+                                     @Nullable String resourceName,
+                                     @Nullable String resourceValue,
+                                     boolean chooseName,
+                                     boolean chooseValue,
+                                     boolean chooseFilename,
+                                     @Nullable VirtualFile defaultFile,
+                                     @Nullable VirtualFile contextFile,
+                                     @NotNull Function<Module, ResourceNameValidator> nameValidatorFactory) {
+    setupUi();
+    setChangeNameVisible(false);
+    setChangeValueVisible(false);
+    setChangeFileNameVisible(chooseFilename);
+    myModule = module;
+    myContextFile = contextFile;
+    if (chooseName) {
+      setChangeNameVisible(true);
+      if (!StringUtil.isEmpty(resourceName)) {
+        myNameField.setText(resourceName);
+      }
+    }
+
+    if (chooseValue) {
+      setChangeValueVisible(true);
+      if (!StringUtil.isEmpty(resourceValue)) {
+        myValueField.setText(resourceValue);
+      }
+    }
+
+    myResourceType = resourceType;
+
+    ApplicationManager.getApplication().assertReadAccessAllowed();
+    // Set up UI to choose the base directory if needed (use context to prune selection).
+    myResDirCombo.setVisible(false);
+    myResDirLabel.setVisible(false);
+    setupResourceDirectoryCombo();
+
+    if (defaultFile == null) {
+      final String defaultFileName = AndroidResourceUtil.getDefaultResourceFileName(myResourceType);
+
+      if (defaultFileName != null) {
+        myFileNameCombo.getEditor().setItem(defaultFileName);
+      }
+    }
+
+    myDirectoriesLabel.setLabelFor(myDirectoriesPanel);
+    mySubdirPanel = new CreateXmlResourceSubdirPanel(module.getProject(), folderType, myDirectoriesPanel, this);
+    myResourceNameValidator = nameValidatorFactory.apply(getModule());
+
+    if (defaultFile != null) {
+      resetFromFile(defaultFile, module.getProject());
+    }
+  }
+
+  private void setupResourceDirectoryCombo() {
+    Project project = myModule.getProject();
+    if (myContextFile != null) {
+      // Try to figure out res/ dir from context (e.g., while refactoring an xml file that's a child of a res/ directory).
+      // We currently take the parent and hide the combo box.
+      PsiManager manager = PsiManager.getInstance(project);
+      VirtualFile virtualDirectory = BlazeCreateResourceUtils.getResDirFromDataContext(myContextFile);
+      PsiDirectory directory = virtualDirectory != null ? manager.findDirectory(virtualDirectory) : null;
+      if (directory != null) {
+        myResDirectory = directory.getVirtualFile();
+      }
+      else {
+        // As a last resort, if we have poor context, e.g., quick fix from within a .java file, set up the UI
+        // based on the deps of the .java file.
+        BlazeCreateResourceUtils.setupResDirectoryChoices(project, myContextFile, myResDirLabel, myResDirCombo);
+      }
+    }
+    else {
+      // As a last resort, if we have no context at all, set up some UI.
+      BlazeCreateResourceUtils.setupResDirectoryChoices(project, null, myResDirLabel, myResDirCombo);
+    }
+  }
+
+  @Override
+  public void resetToDefault() {
+    String defaultFileName = AndroidResourceUtil.getDefaultResourceFileName(myResourceType);
+    if (defaultFileName != null) {
+      myFileNameCombo.getEditor().setItem(defaultFileName);
+    }
+    setupResourceDirectoryCombo();
+    mySubdirPanel.resetToDefault();
+  }
+
+  @Override
+  public void resetFromFile(@NotNull VirtualFile file, @NotNull Project project) {
+    final VirtualFile parent = file.getParent();
+    if (parent == null) {
+      return;
+    }
+    mySubdirPanel.resetFromFile(parent);
+    myFileNameCombo.getEditor().setItem(file.getName());
+    setupResourceDirectoryCombo();
+    myPanel.repaint();
+  }
+
+  @Override
+  public String getResourceName() {
+    return myNameField.getText().trim();
+  }
+
+  @Override
+  public ResourceType getType() {
+    return myResourceType;
+  }
+
+  @Override
+  public String getValue() {
+    return myValueField.getText().trim();
+  }
+
+  @Override
+  public VirtualFile getResourceDirectory() {
+    if (myResDirectory != null) {
+      return myResDirectory;
+    }
+    if (myResDirCombo.isVisible()) {
+      PsiDirectory directory = BlazeCreateResourceUtils.getResDirFromUI(myModule.getProject(), myResDirCombo);
+      return directory != null ? directory.getVirtualFile() : null;
+    }
+    return null;
+  }
+
+  @Override
+  public List<String> getDirNames() {
+    return mySubdirPanel.getDirNames();
+  }
+
+  @Override
+  public String getFileName() {
+    return ((String)myFileNameCombo.getEditor().getItem()).trim();
+  }
+
+  @Override
+  public ValidationInfo doValidate() {
+    String resourceName = getResourceName();
+    VirtualFile resourceDir = getResourceDirectory();
+    List<String> directoryNames = getDirNames();
+    String fileName = getFileName();
+
+    if (myNameField.isVisible() && resourceName.isEmpty()) {
+      return new ValidationInfo("specify resource name", myNameField);
+    }
+    else if (myNameField.isVisible() && !AndroidResourceUtil.isCorrectAndroidResourceName(resourceName)) {
+      return new ValidationInfo(resourceName + " is not correct resource name", myNameField);
+    }
+    else if (fileName.isEmpty()) {
+      return new ValidationInfo("specify file name", myFileNameCombo);
+    }
+    else if (resourceDir == null) {
+      return new ValidationInfo("specify a resource directory", myResDirCombo);
+    }
+    else if (directoryNames.isEmpty()) {
+      return new ValidationInfo("choose directories", myDirectoriesPanel);
+    }
+
+    return CreateXmlResourceDialog.checkIfResourceAlreadyExists(myModule.getProject(), resourceDir, resourceName,
+                                                                myResourceType, directoryNames, fileName);
+  }
+
+  @Override
+  public ResourceNameValidator getResourceNameValidator() {
+    return myResourceNameValidator;
+  }
+
+  @NotNull
+  @Override
+  public Module getModule() {
+    return myModule;
+  }
+
+  /**
+   * @see CreateXmlResourceDialog#getPreferredFocusedComponent()
+   */
+  @Override
+  public JComponent getPreferredFocusedComponent() {
+    String name = myNameField.getText();
+    if (name.isEmpty()) {
+      return myNameField;
+    }
+    else if (myValueField.isVisible()) {
+      return myValueField;
+    }
+    else {
+      return myFileNameCombo;
+    }
+  }
+
+  @Override
+  public JComponent getPanel() {
+    return myPanel;
+  }
+
+  private void setChangeFileNameVisible(boolean isVisible) {
+    myFileNameLabel.setVisible(isVisible);
+    myFileNameCombo.setVisible(isVisible);
+  }
+
+  private void setChangeValueVisible(boolean isVisible) {
+    myValueField.setVisible(isVisible);
+    myValueLabel.setVisible(isVisible);
+  }
+
+  private void setChangeNameVisible(boolean isVisible) {
+    myNameField.setVisible(isVisible);
+    myNameLabel.setVisible(isVisible);
+  }
+
+  // Only public for CreateXmlResourceSubdirPanel.Parent
+  @Override
+  public void updateFilesCombo(List<VirtualFile> directories) {
+    final Object oldItem = myFileNameCombo.getEditor().getItem();
+    final Set<String> fileNameSet = new HashSet<>();
+
+    for (VirtualFile dir : directories) {
+      for (VirtualFile file : dir.getChildren()) {
+        fileNameSet.add(file.getName());
+      }
+    }
+    final List<String> fileNames = new ArrayList<>(fileNameSet);
+    Collections.sort(fileNames);
+    myFileNameCombo.setModel(new DefaultComboBoxModel(fileNames.toArray()));
+    myFileNameCombo.getEditor().setItem(oldItem);
+  }
+
+  /**
+   * Initially generated by IntelliJ from a .form file.
+   */
+  private void setupUi() {
+    myPanel = new JPanel();
+    myPanel.setLayout(new GridLayoutManager(6, 2, new Insets(0, 0, 5, 0), -1, -1));
+    myNameLabel = new JBLabel();
+    myNameLabel.setText("Resource name:");
+    myNameLabel.setDisplayedMnemonic('N');
+    myNameLabel.setDisplayedMnemonicIndex(9);
+    myPanel.add(myNameLabel,
+                new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myNameField = new JTextField();
+    myPanel.add(myNameField, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                 GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null,
+                                                 new Dimension(150, -1), null, 0, false));
+    myFileNameLabel = new JBLabel();
+    myFileNameLabel.setText("File name:");
+    myFileNameLabel.setDisplayedMnemonic('F');
+    myFileNameLabel.setDisplayedMnemonicIndex(0);
+    myPanel.add(myFileNameLabel,
+                new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myDirectoriesPanel = new JPanel();
+    myDirectoriesPanel.setLayout(new BorderLayout(0, 0));
+    myPanel.add(myDirectoriesPanel, new GridConstraints(5, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH,
+                                                        GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                        GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null,
+                                                        null, null, 0, false));
+    myDirectoriesLabel = new JBLabel();
+    myDirectoriesLabel.setText("Create the resource in directories:");
+    myDirectoriesLabel.setDisplayedMnemonic('C');
+    myDirectoriesLabel.setDisplayedMnemonicIndex(0);
+    myPanel.add(myDirectoriesLabel,
+                new GridConstraints(4, 0, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myValueLabel = new JBLabel();
+    myValueLabel.setText("Resource value:");
+    myValueLabel.setDisplayedMnemonic('V');
+    myValueLabel.setDisplayedMnemonicIndex(9);
+    myPanel.add(myValueLabel,
+                new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myValueField = new JTextField();
+    myPanel.add(myValueField, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                  GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null,
+                                                  new Dimension(150, -1), null, 0, false));
+    myFileNameCombo = new JComboBox();
+    myFileNameCombo.setEditable(true);
+    myPanel.add(myFileNameCombo, new GridConstraints(3, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                     GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null,
+                                                     null, 0, false));
+    myResDirLabel = new JBLabel();
+    myResDirLabel.setText("Base directory:");
+    myResDirLabel.setDisplayedMnemonic('B');
+    myResDirLabel.setDisplayedMnemonicIndex(0);
+    myPanel.add(myResDirLabel,
+                new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myResDirCombo = new ComboboxWithBrowseButton();
+    myPanel.add(myResDirCombo, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                   GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null,
+                                                   0, false));
+    myNameLabel.setLabelFor(myNameField);
+    myFileNameLabel.setLabelFor(myFileNameCombo);
+    myValueLabel.setLabelFor(myValueField);
+  }
+
+}
diff --git a/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeNewResourceCreationHandler.java b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeNewResourceCreationHandler.java
new file mode 100644
index 0000000..03a34a9
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/resources/actions/BlazeNewResourceCreationHandler.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.resources.actions;
+
+import com.android.ide.common.resources.configuration.FolderConfiguration;
+import com.android.resources.ResourceFolderType;
+import com.android.resources.ResourceType;
+import com.android.tools.idea.res.ResourceNameValidator;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import org.jetbrains.android.actions.*;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.function.Function;
+
+/**
+ * Decides which create resource dialogs to use for Blaze projects.
+ */
+public class BlazeNewResourceCreationHandler implements NewResourceCreationHandler {
+
+  @Override
+  public boolean isApplicable(@NotNull Project project) {
+    return Blaze.isBlazeProject(project);
+  }
+
+  @NotNull
+  @Override
+  public CreateResourceDirectoryDialogBase createNewResourceDirectoryDialog(
+    @NotNull Project project,
+    @Nullable Module module,
+    @Nullable ResourceFolderType resType,
+    @Nullable PsiDirectory resDirectory,
+    @Nullable DataContext dataContext,
+    @NotNull CreateResourceDirectoryDialogBase.ValidatorFactory validatorFactory) {
+    return new BlazeCreateResourceDirectoryDialog(project, module, resType, resDirectory, dataContext, validatorFactory);
+  }
+
+  @NotNull
+  @Override
+  public CreateResourceFileDialogBase createNewResourceFileDialog(
+    @NotNull AndroidFacet facet,
+    @NotNull Collection<CreateTypedResourceFileAction> actions,
+    @Nullable ResourceFolderType folderType,
+    @Nullable String filename,
+    @Nullable String rootElement,
+    @Nullable FolderConfiguration folderConfiguration,
+    boolean chooseFileName,
+    boolean chooseModule,
+    @Nullable PsiDirectory resDirectory,
+    @Nullable DataContext dataContext,
+    @NotNull CreateResourceFileDialogBase.ValidatorFactory validatorFactory) {
+    return new BlazeCreateResourceFileDialog(facet, actions, folderType, filename, rootElement, folderConfiguration, chooseFileName,
+                                             chooseModule, resDirectory, dataContext, validatorFactory);
+  }
+
+  @Override
+  public CreateXmlResourcePanel createNewResourceValuePanel(
+    @NotNull Module module,
+    @NotNull ResourceType resourceType,
+    @NotNull ResourceFolderType folderType,
+    @Nullable String resourceName,
+    @Nullable String resourceValue,
+    boolean chooseName,
+    boolean chooseValue,
+    boolean chooseFilename,
+    @Nullable VirtualFile defaultFile,
+    @Nullable VirtualFile contextFile,
+    @NotNull Function<Module, ResourceNameValidator> nameValidatorFactory) {
+    return new BlazeCreateXmlResourcePanel(module, resourceType, folderType, resourceName, resourceValue,
+                                           chooseName, chooseValue, chooseFilename, defaultFile, contextFile, nameValidatorFactory);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfiguration.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfiguration.java
new file mode 100644
index 0000000..ff5dc7f
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfiguration.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.idea.blaze.android.run;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.facet.AndroidFacet;
+
+/**
+ * Common interface between android_binary and android_test configurations.
+ */
+public interface BlazeAndroidRunConfiguration extends BlazeRunConfiguration {
+
+  BlazeAndroidRunContext createRunContext(Project project,
+                                          AndroidFacet facet,
+                                          ExecutionEnvironment env,
+                                          ImmutableList<String> buildFlags);
+
+  BlazeAndroidRunConfigurationRunner getRunner();
+
+  BlazeAndroidRunConfigurationCommonState getCommonState();
+
+  @Override
+  Label getTarget();
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
new file mode 100644
index 0000000..06b4262
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonState.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run;
+
+import com.android.tools.idea.run.ValidationError;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.DefaultJDOMExternalizer;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.JDOMExternalizable;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+import static com.google.idea.blaze.android.cppapi.NdkSupport.NDK_SUPPORT;
+
+/**
+ * A shared state class for run configurations targeting Blaze Android rules.
+ * We implement the deprecated JDomExternalizable to fit with the other run configs.
+ */
+public class BlazeAndroidRunConfigurationCommonState implements JDOMExternalizable {
+  private static final String TARGET_ATTR = "blaze-target";
+  private static final String USER_FLAG_TAG = "blaze-user-flag";
+  private static final String NATIVE_DEBUG_ATTR = "blaze-native-debug";
+
+  @Nullable private Label target;
+  private List<String> userFlags;
+  private boolean nativeDebuggingEnabled = false;
+
+  /**
+   * Creates a configuration state initialized with the given rule and flags.
+   */
+  public BlazeAndroidRunConfigurationCommonState(@Nullable Label target, List<String> userFlags) {
+    this.target = target;
+    this.userFlags = userFlags;
+  }
+
+  @Nullable
+  public Label getTarget() {
+    return target;
+  }
+
+  public void setTarget(@Nullable Label target) {
+    this.target = target;
+  }
+
+  public List<String> getUserFlags() {
+    return userFlags;
+  }
+
+  public void setUserFlags(List<String> userFlags) {
+    this.userFlags = userFlags;
+  }
+
+  public boolean isNativeDebuggingEnabled() {
+    return nativeDebuggingEnabled && NDK_SUPPORT.getValue();
+  }
+
+  public void setNativeDebuggingEnabled(boolean nativeDebuggingEnabled) {
+    this.nativeDebuggingEnabled = nativeDebuggingEnabled;
+  }
+
+  public void checkConfiguration(Project project, Kind kind, List<ValidationError> errors) {
+    RuleIdeInfo rule = target != null ? RuleFinder.getInstance().ruleForTarget(project, target) : null;
+    if (rule == null) {
+      errors.add(ValidationError.fatal(
+        String.format("No existing %s rule selected.", Blaze.buildSystemName(project))
+      ));
+    }
+    else if (!rule.kindIsOneOf(kind)) {
+      errors.add(ValidationError.fatal(
+        String.format("Selected %s rule is not %s", Blaze.buildSystemName(project), kind.toString())
+      ));
+    }
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    DefaultJDOMExternalizer.readExternal(this, element);
+
+    target = null;
+    String targetString = element.getAttributeValue(TARGET_ATTR);
+    if (targetString != null) {
+      try {
+        target = new Label(targetString);
+      }
+      catch (IllegalArgumentException e) {
+        throw new InvalidDataException("Bad configuration target", e);
+      }
+    }
+    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
+    for (Element e : element.getChildren(USER_FLAG_TAG)) {
+      String flag = e.getTextTrim();
+      if (flag != null && !flag.isEmpty()) {
+        flagsBuilder.add(flag);
+      }
+    }
+    userFlags = flagsBuilder.build();
+    setNativeDebuggingEnabled(Boolean.parseBoolean(element.getAttributeValue(NATIVE_DEBUG_ATTR)));
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    DefaultJDOMExternalizer.writeExternal(this, element);
+
+    if (target != null) {
+      element.setAttribute(TARGET_ATTR, target.toString());
+    }
+    for (String flag : userFlags) {
+      Element child = new Element(USER_FLAG_TAG);
+      child.setText(flag);
+      element.addContent(child);
+    }
+    element.setAttribute(NATIVE_DEBUG_ATTR, Boolean.toString(nativeDebuggingEnabled));
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateEditor.java
new file mode 100644
index 0000000..04b74ae
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeAndroidRunConfigurationCommonStateEditor.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.idea.blaze.android.cppapi.NdkSupport;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.ui.ComboWrapper;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.Project;
+import com.intellij.ui.ListCellRendererWrapper;
+import com.intellij.util.execution.ParametersListUtil;
+
+import javax.swing.*;
+import java.util.List;
+
+/**
+ * A simplified, Blaze-specific variant of
+ * {@link org.jetbrains.android.run.AndroidRunConfigurationEditor}.
+ */
+public class BlazeAndroidRunConfigurationCommonStateEditor {
+  private static final Ordering<RuleIdeInfo> ALPHABETICAL = Ordering.usingToString().onResultOf((ruleIdeInfo) -> ruleIdeInfo.label);
+
+  private final Project project;
+  private final Kind kind;
+  private final ComboWrapper<RuleIdeInfo> ruleCombo;
+  private final JTextArea userFlagsField;
+  private final JCheckBox enableNativeDebuggingCheckBox;
+
+  public BlazeAndroidRunConfigurationCommonStateEditor(
+    Project project,
+    Kind kind) {
+    this.project = project;
+    this.kind = kind;
+
+    ruleCombo = ComboWrapper.create();
+    List<RuleIdeInfo> rules = ALPHABETICAL.sortedCopy(RuleFinder.getInstance().rulesOfKinds(project, kind));
+    ruleCombo.setItems(rules);
+    ruleCombo.setRenderer(new ListCellRendererWrapper<RuleIdeInfo>() {
+      @Override
+      public void customize(JList list, RuleIdeInfo value, int index,
+                            boolean selected, boolean hasFocus) {
+        setText(value == null ? "" : value.label.toString());
+      }
+    });
+
+    userFlagsField = new JTextArea(3 /* rows */, 50 /* columns */);
+    userFlagsField.setToolTipText("e.g. --config=android_arm");
+    enableNativeDebuggingCheckBox = new JCheckBox("Enable native debugging", false);
+  }
+
+  public void resetEditorFrom(BlazeAndroidRunConfigurationCommonState runConfigurationState) {
+    Label target = runConfigurationState.getTarget();
+    RuleIdeInfo rule = target != null ? RuleFinder.getInstance().ruleForTarget(project, target) : null;
+    ruleCombo.setSelectedItem(rule);
+    userFlagsField.setText(ParametersListUtil.join(runConfigurationState.getUserFlags()));
+    enableNativeDebuggingCheckBox.setSelected(runConfigurationState.isNativeDebuggingEnabled());
+  }
+
+  public void applyEditorTo(BlazeAndroidRunConfigurationCommonState runConfigurationState)
+    throws ConfigurationException {
+    RuleIdeInfo rule = ruleCombo.getSelectedItem();
+    Label target = rule != null ? rule.label : null;
+    runConfigurationState.setTarget(target);
+    List<String> userFlags = ParametersListUtil.parse(Strings.nullToEmpty(userFlagsField.getText()));
+    runConfigurationState.setUserFlags(userFlags);
+    runConfigurationState.setNativeDebuggingEnabled(enableNativeDebuggingCheckBox.isSelected());
+  }
+
+  public List<JComponent> getComponents() {
+    List<JComponent> result = Lists.newArrayList(
+      new JLabel(kind.toString() + " rule:"),
+      ruleCombo.getCombo(),
+      new JLabel(String.format("Custom %s build flags:", Blaze.buildSystemName(project))),
+      userFlagsField
+    );
+
+    if (NdkSupport.NDK_SUPPORT.getValue()) {
+       result.add(enableNativeDebuggingCheckBox);
+    }
+    return result;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/BlazeBeforeRunTaskProvider.java b/aswb/src/com/google/idea/blaze/android/run/BlazeBeforeRunTaskProvider.java
new file mode 100644
index 0000000..87358f5
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/BlazeBeforeRunTaskProvider.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run;
+
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.BeforeRunTask;
+import com.intellij.execution.BeforeRunTaskProvider;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import icons.BlazeIcons;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * Provides a before run task provider that immediately transfers control
+ * to {@link BlazeAndroidRunConfigurationRunner}
+ */
+public final class BlazeBeforeRunTaskProvider extends BeforeRunTaskProvider<BlazeBeforeRunTaskProvider.Task> {
+  @NotNull
+  public static final Key<Task> ID = Key.create("Android.Blaze.BeforeRunTask");
+  private static final String TASK_NAME = "Blaze before-run task";
+
+  public static class Task extends BeforeRunTask<Task> {
+    private Task() {
+      super(ID);
+      setEnabled(true);
+    }
+  }
+
+  @NotNull
+  private final Project project;
+
+  public BlazeBeforeRunTaskProvider(@NotNull Project project) {
+    this.project = project;
+  }
+
+  @Override
+  public Key<Task> getId() {
+    return ID;
+  }
+
+  @Nullable
+  @Override
+  public Icon getIcon() {
+    return BlazeIcons.Blaze;
+  }
+
+  @Nullable
+  @Override
+  public Icon getTaskIcon(Task task) {
+    return BlazeIcons.Blaze;
+  }
+
+  @Override
+  public String getName() {
+    return Blaze.guessBuildSystemName() + "before-run task";
+  }
+
+  @Override
+  public String getDescription(Task task) {
+    return Blaze.guessBuildSystemName() + "before-run task";
+  }
+
+  @Override
+  public boolean isConfigurable() {
+    return false;
+  }
+
+  @Nullable
+  @Override
+  public Task createTask(RunConfiguration runConfiguration) {
+    if (runConfiguration instanceof BlazeAndroidRunConfiguration) {
+      return new Task();
+    }
+    else {
+      return null;
+    }
+  }
+
+  @Override
+  public boolean configureTask(RunConfiguration runConfiguration, Task task) {
+    return false;
+  }
+
+  @Override
+  public boolean canExecuteTask(RunConfiguration configuration, Task task) {
+    return configuration instanceof BlazeAndroidRunConfiguration;
+  }
+
+  @Override
+  public boolean executeTask(
+    final DataContext dataContext,
+    final RunConfiguration configuration,
+    final ExecutionEnvironment env,
+    Task task) {
+    if (!canExecuteTask(configuration, task)) {
+      return false;
+    }
+
+    final BlazeAndroidRunConfiguration blazeConfiguration = (BlazeAndroidRunConfiguration)configuration;
+    return blazeConfiguration.getRunner().executeBuild(env);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationIdProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationIdProvider.java
new file mode 100644
index 0000000..51a0f66
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationIdProvider.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.android.tools.idea.run.ApkProvisionException;
+import com.android.tools.idea.run.ApplicationIdProvider;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
+import org.jetbrains.android.dom.manifest.Manifest;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Application id provider for android_binary.
+ */
+public class BlazeAndroidBinaryApplicationIdProvider implements ApplicationIdProvider {
+  private final Project project;
+  private final ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture;
+
+  public BlazeAndroidBinaryApplicationIdProvider(Project project,
+                                                 ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture) {
+    this.project = project;
+    this.deployInfoFuture = deployInfoFuture;
+  }
+
+  @NotNull
+  @Override
+  public String getPackageName() throws ApkProvisionException {
+    BlazeAndroidDeployInfo deployInfo = Futures.get(deployInfoFuture, ApkProvisionException.class);
+    Manifest manifest = deployInfo.getMergedManifest();
+    if (manifest == null) {
+      throw new ApkProvisionException("Could not find merged manifest: " + deployInfo.getMergedManifestFile());
+    }
+    String applicationId = ApplicationManager.getApplication().runReadAction(
+      (Computable<String>)() -> manifest.getPackage().getValue()
+    );
+    if (applicationId == null) {
+      throw new ApkProvisionException("No application id in merged manifest: " + deployInfo.getMergedManifestFile());
+    }
+    return applicationId;
+  }
+
+  @Nullable
+  @Override
+  public String getTestPackageName() throws ApkProvisionException {
+    return null;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationLaunchTaskProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationLaunchTaskProvider.java
new file mode 100644
index 0000000..9233f20
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryApplicationLaunchTaskProvider.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.android.tools.idea.run.ApkProvisionException;
+import com.android.tools.idea.run.ApplicationIdProvider;
+import com.android.tools.idea.run.activity.StartActivityFlagsProvider;
+import com.android.tools.idea.run.tasks.AndroidDeepLinkLaunchTask;
+import com.android.tools.idea.run.tasks.DefaultActivityLaunchTask;
+import com.android.tools.idea.run.tasks.LaunchTask;
+import com.android.tools.idea.run.tasks.SpecificActivityLaunchTask;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+
+import java.io.File;
+
+/**
+ * Provides the launch task for android_binary
+ */
+public class BlazeAndroidBinaryApplicationLaunchTaskProvider {
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidBinaryApplicationLaunchTaskProvider.class);
+
+  public static LaunchTask getApplicationLaunchTask(Project project,
+                                                    ApplicationIdProvider applicationIdProvider,
+                                                    File mergedManifestFile,
+                                                    BlazeAndroidBinaryRunConfigurationState configState,
+                                                    StartActivityFlagsProvider startActivityFlagsProvider,
+                                                    ProcessHandlerLaunchStatus processHandlerLaunchStatus) {
+    try {
+      String applicationId = applicationIdProvider.getPackageName();
+
+      final LaunchTask launchTask;
+
+      switch (configState.MODE) {
+        case BlazeAndroidBinaryRunConfigurationState.DO_NOTHING:
+          launchTask = null;
+          break;
+        case BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEFAULT_ACTIVITY:
+          BlazeDefaultActivityLocator activityLocator = new BlazeDefaultActivityLocator(project, mergedManifestFile);
+          launchTask = new DefaultActivityLaunchTask(applicationId, activityLocator, startActivityFlagsProvider);
+          break;
+        case BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY:
+          launchTask = new SpecificActivityLaunchTask(applicationId, configState.ACTIVITY_CLASS, startActivityFlagsProvider);
+          break;
+        case BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEEP_LINK:
+          launchTask = new AndroidDeepLinkLaunchTask(configState.DEEP_LINK, startActivityFlagsProvider);
+          break;
+        default:
+          launchTask = null;
+          break;
+      }
+      return launchTask;
+    }
+    catch (ApkProvisionException e) {
+      LOG.error(e);
+      processHandlerLaunchStatus.terminateLaunch("Unable to identify application id");
+      return null;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryConsoleProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryConsoleProvider.java
new file mode 100644
index 0000000..776f16d
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryConsoleProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.android.tools.idea.run.ConsoleProvider;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.filters.TextConsoleBuilder;
+import com.intellij.execution.filters.TextConsoleBuilderFactory;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Console provider for android_binary
+ */
+public class BlazeAndroidBinaryConsoleProvider implements ConsoleProvider {
+  private final Project project;
+
+  public BlazeAndroidBinaryConsoleProvider(Project project) {
+    this.project = project;
+  }
+
+  @NotNull
+  @Override
+  public ConsoleView createAndAttach(@NotNull Disposable parent,
+                                     @NotNull ProcessHandler handler,
+                                     @NotNull Executor executor)
+    throws ExecutionException {
+    final TextConsoleBuilder builder = TextConsoleBuilderFactory.getInstance().createBuilder(project);
+    ConsoleView console = builder.getConsole();
+    console.attachToProcess(handler);
+    return console;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java
new file mode 100644
index 0000000..6adef27
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryNormalBuildRunContext.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.*;
+import com.android.tools.idea.run.activity.DefaultStartActivityFlagsProvider;
+import com.android.tools.idea.run.activity.StartActivityFlagsProvider;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.tasks.DebugConnectorTask;
+import com.android.tools.idea.run.tasks.DeployApkTask;
+import com.android.tools.idea.run.tasks.LaunchTask;
+import com.android.tools.idea.run.tasks.LaunchTasksProvider;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.google.idea.blaze.android.run.deployinfo.BlazeApkProvider;
+import com.google.idea.blaze.android.run.runner.*;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Run context for android_binary.
+ */
+class BlazeAndroidBinaryNormalBuildRunContext implements BlazeAndroidRunContext {
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidBinaryNormalBuildRunContext.class);
+
+  private final Project project;
+  private final AndroidFacet facet;
+  private final RunConfiguration runConfiguration;
+  private final ExecutionEnvironment env;
+  private final BlazeAndroidBinaryRunConfigurationState configState;
+  private final ConsoleProvider consoleProvider;
+  private final BlazeApkBuildStepNormalBuild buildStep;
+  private final BlazeApkProvider apkProvider;
+  private final ApplicationIdProvider applicationIdProvider;
+
+  public BlazeAndroidBinaryNormalBuildRunContext(Project project,
+                                                 AndroidFacet facet,
+                                                 RunConfiguration runConfiguration,
+                                                 ExecutionEnvironment env,
+                                                 BlazeAndroidRunConfigurationCommonState commonState,
+                                                 BlazeAndroidBinaryRunConfigurationState configState,
+                                                 ImmutableList<String> buildFlags) {
+    this.project = project;
+    this.facet = facet;
+    this.runConfiguration = runConfiguration;
+    this.env = env;
+    this.configState = configState;
+    this.consoleProvider = new BlazeAndroidBinaryConsoleProvider(project);
+    this.buildStep = new BlazeApkBuildStepNormalBuild(project, commonState, buildFlags);
+    this.apkProvider = new BlazeApkProvider(project, buildStep.getDeployInfo());
+    this.applicationIdProvider = new BlazeAndroidBinaryApplicationIdProvider(project, buildStep.getDeployInfo());
+  }
+
+  @Override
+  public void augmentEnvironment(ExecutionEnvironment env) {
+  }
+
+  @Override
+  public BlazeAndroidDeviceSelector getDeviceSelector() {
+    return new BlazeAndroidDeviceSelector.NormalDeviceSelector();
+  }
+
+  @Override
+  public void augmentLaunchOptions(@NotNull LaunchOptions.Builder options) {
+    options
+      .setDeploy(true)
+      .setPmInstallOptions(configState.ACTIVITY_EXTRA_FLAGS)
+      .setOpenLogcatAutomatically(true);
+  }
+
+  @NotNull
+  @Override
+  public ConsoleProvider getConsoleProvider() {
+    return consoleProvider;
+  }
+
+  @Override
+  public ApplicationIdProvider getApplicationIdProvider() throws ExecutionException {
+    return applicationIdProvider;
+  }
+
+  @Override
+  public BlazeApkBuildStep getBuildStep() {
+    return buildStep;
+  }
+
+  @Override
+  public LaunchTasksProvider getLaunchTasksProvider(
+    LaunchOptions launchOptions,
+    BlazeAndroidRunConfigurationDebuggerManager debuggerManager) throws ExecutionException {
+    return new BlazeAndroidLaunchTasksProvider(project, this, applicationIdProvider, launchOptions, debuggerManager);
+  }
+
+  @Nullable
+  @Override
+  public ImmutableList<LaunchTask> getDeployTasks(IDevice device, LaunchOptions launchOptions) throws ExecutionException {
+    Collection<ApkInfo> apks;
+    try {
+      apks = apkProvider.getApks(device);
+    }
+    catch (ApkProvisionException e) {
+      throw new ExecutionException(e);
+    }
+    return ImmutableList.of(new DeployApkTask(project, launchOptions, apks));
+  }
+
+  @Override
+  public LaunchTask getApplicationLaunchTask(LaunchOptions launchOptions,
+                                             AndroidDebugger androidDebugger,
+                                             AndroidDebuggerState androidDebuggerState,
+                                             ProcessHandlerLaunchStatus processHandlerLaunchStatus) throws ExecutionException {
+    final StartActivityFlagsProvider startActivityFlagsProvider = new DefaultStartActivityFlagsProvider(
+      androidDebugger,
+      androidDebuggerState,
+      project,
+      launchOptions.isDebug(),
+      configState.ACTIVITY_EXTRA_FLAGS
+    );
+
+    BlazeAndroidDeployInfo deployInfo = Futures.get(buildStep.getDeployInfo(), ExecutionException.class);
+
+    return BlazeAndroidBinaryApplicationLaunchTaskProvider.getApplicationLaunchTask(
+      project,
+      applicationIdProvider,
+      deployInfo.getMergedManifestFile(),
+      configState,
+      startActivityFlagsProvider,
+      processHandlerLaunchStatus
+    );
+  }
+
+  @Nullable
+  @Override
+  public DebugConnectorTask getDebuggerTask(LaunchOptions launchOptions,
+                                            AndroidDebugger androidDebugger,
+                                            AndroidDebuggerState androidDebuggerState,
+                                            Set<String> packageIds) throws ExecutionException {
+    //noinspection unchecked
+    return androidDebugger.getConnectDebuggerTask(env, null, packageIds,facet, androidDebuggerState, runConfiguration.getType().getId());
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java
new file mode 100644
index 0000000..b178b12
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryProgramRunner.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.android.tools.idea.fd.InstantRunUtils;
+import com.android.tools.idea.run.AndroidSessionInfo;
+import com.google.idea.blaze.android.run.binary.mobileinstall.IncrementalInstallDebugExecutor;
+import com.google.idea.blaze.android.run.binary.mobileinstall.IncrementalInstallRunExecutor;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import com.intellij.execution.executors.DefaultRunExecutor;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.runners.DefaultProgramRunner;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.ui.RunContentDescriptor;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Program runner for {@link BlazeAndroidBinaryRunConfiguration}
+ */
+public class BlazeAndroidBinaryProgramRunner extends DefaultProgramRunner {
+  @Override
+  public boolean canRun(@NotNull String executorId, @NotNull RunProfile profile) {
+    if (!(profile instanceof BlazeAndroidBinaryRunConfiguration)) {
+      return false;
+    }
+    BlazeAndroidBinaryRunConfiguration runConfiguration = (BlazeAndroidBinaryRunConfiguration) profile;
+    if (runConfiguration.getConfigState().isMobileInstall()) {
+      return (IncrementalInstallDebugExecutor.EXECUTOR_ID.equals(executorId)
+              || IncrementalInstallRunExecutor.EXECUTOR_ID.equals(executorId));
+    }
+
+    return DefaultDebugExecutor.EXECUTOR_ID.equals(executorId) || DefaultRunExecutor.EXECUTOR_ID.equals(executorId);
+  }
+
+  @Override
+  protected RunContentDescriptor doExecute(@NotNull final RunProfileState state, @NotNull final ExecutionEnvironment env)
+    throws ExecutionException {
+    RunContentDescriptor descriptor = super.doExecute(state, env);
+    if (descriptor != null) {
+      ProcessHandler processHandler = descriptor.getProcessHandler();
+      assert processHandler != null;
+
+      RunProfile runProfile = env.getRunProfile();
+      int uniqueId = (runProfile instanceof BlazeAndroidBinaryRunConfiguration)
+                     ? ((BlazeAndroidBinaryRunConfiguration)runProfile).getUniqueID() : -1;
+      AndroidSessionInfo sessionInfo = new AndroidSessionInfo(processHandler, descriptor, uniqueId, env.getExecutor().getId(),
+                                                              InstantRunUtils.isInstantRunEnabled(env));
+      processHandler.putUserData(AndroidSessionInfo.KEY, sessionInfo);
+    }
+
+    return descriptor;
+  }
+
+  @Override
+  @NotNull
+  public String getRunnerId() {
+    return "AndroidProgramRunner";
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfiguration.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfiguration.java
new file mode 100644
index 0000000..7a02c45
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfiguration.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.android.sdklib.AndroidVersion;
+import com.android.tools.idea.fd.InstantRunManager;
+import com.android.tools.idea.run.AndroidSessionInfo;
+import com.android.tools.idea.run.ValidationError;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfiguration;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.binary.instantrun.BlazeAndroidBinaryInstantRunContext;
+import com.google.idea.blaze.android.run.binary.mobileinstall.BlazeAndroidBinaryMobileInstallRunContext;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
+import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.RunnerIconProvider;
+import com.intellij.execution.configurations.*;
+import com.intellij.execution.executors.DefaultRunExecutor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.options.SettingsEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import icons.AndroidIcons;
+import org.jdom.Element;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.List;
+
+/**
+ * An extension of the normal Android Studio run configuration for launching Android applications,
+ * adapted specifically for selecting and launching android_binary targets.
+ */
+public final class BlazeAndroidBinaryRunConfiguration extends LocatableConfigurationBase
+  implements BlazeAndroidRunConfiguration, RunConfiguration, RunnerIconProvider {
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidBinaryRunConfiguration.class);
+  private final Project project;
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+  private final BlazeAndroidBinaryRunConfigurationState configState = new BlazeAndroidBinaryRunConfigurationState();
+  private final BlazeAndroidRunConfigurationRunner runner;
+
+  public BlazeAndroidBinaryRunConfiguration(Project project, ConfigurationFactory factory) {
+    super(project, factory, "");
+    this.project = project;
+
+    RuleIdeInfo rule = RuleFinder.getInstance().firstRuleOfKinds(project, Kind.ANDROID_BINARY);
+    this.commonState = new BlazeAndroidRunConfigurationCommonState(rule != null ? rule.label : null, ImmutableList.of());
+    this.runner = new BlazeAndroidRunConfigurationRunner(project, this, this.commonState, false, getUniqueID());
+  }
+
+  @Nullable
+  @Override
+  public final Label getTarget() {
+    return commonState.getTarget();
+  }
+
+  public final void setTarget(@Nullable Label target) {
+    commonState.setTarget(target);
+  }
+
+  @Override
+  public BlazeAndroidRunConfigurationCommonState getCommonState() {
+    return commonState;
+  }
+
+  @Override
+  public BlazeAndroidRunConfigurationRunner getRunner() {
+    return runner;
+  }
+
+  @Override
+  public BlazeAndroidRunContext createRunContext(Project project,
+                                                 AndroidFacet facet,
+                                                 ExecutionEnvironment env,
+                                                 ImmutableList<String> buildFlags) {
+    if (configState.isInstantRun()) {
+      return new BlazeAndroidBinaryInstantRunContext(project, facet, this, env, commonState, configState, buildFlags);
+    }
+    else if (configState.isMobileInstall()) {
+      return new BlazeAndroidBinaryMobileInstallRunContext(project, facet, this, env, commonState, configState, buildFlags);
+    }
+    else {
+      return new BlazeAndroidBinaryNormalBuildRunContext(project, facet, this, env, commonState, configState, buildFlags);
+    }
+  }
+
+  @Override
+  @NotNull
+  public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
+    return new BlazeAndroidBinaryRunConfigurationEditor(
+      getProject(),
+      new BlazeAndroidBinaryRunConfigurationStateEditor(getProject())
+    );
+  }
+
+  @Override
+  public final void checkConfiguration() throws RuntimeConfigurationException {
+    List<ValidationError> errors = validate();
+    if (errors.isEmpty()) {
+      return;
+    }
+    // TODO: Do something with the extra error information? Error count?
+    ValidationError topError = Ordering.natural().max(errors);
+    if (topError.isFatal()) {
+      throw new RuntimeConfigurationError(topError.getMessage(), topError.getQuickfix());
+    }
+    throw new RuntimeConfigurationWarning(topError.getMessage(), topError.getQuickfix());
+  }
+
+  private List<ValidationError> validate() {
+    List<ValidationError> errors = Lists.newArrayList();
+    errors.addAll(runner.validate(getModule()));
+    commonState.checkConfiguration(getProject(), Kind.ANDROID_BINARY, errors);
+    return errors;
+  }
+
+  private Module getModule() {
+    return BlazeAndroidProjectStructureSyncer.ensureRunConfigurationModule(project, getTarget());
+  }
+
+  @Override
+  @Nullable
+  public final RunProfileState getState(@NotNull final Executor executor, @NotNull ExecutionEnvironment env) throws ExecutionException {
+    final Module module = getModule();
+    return runner.getState(module, executor, env);
+  }
+
+  @NotNull
+  public BlazeAndroidBinaryRunConfigurationState getConfigState() {
+    return configState;
+  }
+
+  @Nullable
+  @Override
+  public Icon getExecutorIcon(@NotNull RunConfiguration configuration, @NotNull Executor executor) {
+    if (!configState.isInstantRun()) {
+      return null;
+    }
+
+    AndroidSessionInfo info = AndroidSessionInfo.findOldSession(getProject(), null, getUniqueID());
+    if (info == null || !info.isInstantRun() || !info.getExecutorId().equals(executor.getId())) {
+      return null;
+    }
+
+    // Make sure instant run is supported on the relevant device, if found.
+    AndroidVersion androidVersion = InstantRunManager.getMinDeviceApiLevel(info.getProcessHandler());
+    if (!InstantRunManager.isInstantRunCapableDeviceVersion(androidVersion)) {
+      return null;
+    }
+
+    return executor instanceof DefaultRunExecutor
+           ? AndroidIcons.RunIcons.Replay
+           : AndroidIcons.RunIcons.DebugReattach;
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    super.readExternal(element);
+
+    commonState.readExternal(element);
+    runner.readExternal(element);;
+    configState.readExternal(element);
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    super.writeExternal(element);
+
+    commonState.writeExternal(element);
+    runner.writeExternal(element);;
+    configState.writeExternal(element);
+  }
+
+  @Override
+  public RunConfiguration clone() {
+    final Element element = new Element("dummy");
+    try {
+      writeExternal(element);
+      BlazeAndroidBinaryRunConfiguration clone = new BlazeAndroidBinaryRunConfiguration(
+        getProject(), getFactory());
+      clone.readExternal(element);
+      return clone;
+    } catch (InvalidDataException e) {
+      LOG.error(e);
+      return null;
+    } catch (WriteExternalException e) {
+      LOG.error(e);
+      return null;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationEditor.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationEditor.java
new file mode 100644
index 0000000..54d0ee8
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationEditor.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonStateEditor;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.options.SettingsEditor;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.List;
+
+/**
+ * A simplified, Blaze-specific variant of
+ * {@link org.jetbrains.android.run.AndroidRunConfigurationEditor}.
+ */
+class BlazeAndroidBinaryRunConfigurationEditor extends SettingsEditor<BlazeAndroidBinaryRunConfiguration> {
+
+  private final BlazeAndroidBinaryRunConfigurationStateEditor kindSpecificEditor;
+  private final BlazeAndroidRunConfigurationCommonStateEditor commonStateEditor;
+
+  public BlazeAndroidBinaryRunConfigurationEditor(
+    Project project,
+    BlazeAndroidBinaryRunConfigurationStateEditor kindSpecificEditor) {
+    this.kindSpecificEditor = kindSpecificEditor;
+    this.commonStateEditor = new BlazeAndroidRunConfigurationCommonStateEditor(project, Kind.ANDROID_BINARY);
+  }
+
+  @Override
+  protected void resetEditorFrom(BlazeAndroidBinaryRunConfiguration configuration) {
+    commonStateEditor.resetEditorFrom(configuration.getCommonState());
+    kindSpecificEditor.resetFrom(configuration);
+  }
+
+  @Override
+  protected void applyEditorTo(@NotNull BlazeAndroidBinaryRunConfiguration configuration)
+    throws ConfigurationException {
+    commonStateEditor.applyEditorTo(configuration.getCommonState());
+    kindSpecificEditor.applyTo(configuration);
+  }
+
+  @Override
+  @NotNull
+  protected JComponent createEditor() {
+    List<Component> components = Lists.newArrayList();
+    components.addAll(commonStateEditor.getComponents());
+    components.add(kindSpecificEditor.getComponent());
+    return UiUtil.createBox(components);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java
new file mode 100644
index 0000000..e048889
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationState.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.intellij.openapi.util.DefaultJDOMExternalizer;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.JDOMExternalizable;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+
+/**
+ * State specific to the android binary run configuration.
+ */
+public final class BlazeAndroidBinaryRunConfigurationState implements JDOMExternalizable {
+  public static final String LAUNCH_DEFAULT_ACTIVITY = "default_activity";
+  public static final String LAUNCH_SPECIFIC_ACTIVITY = "specific_activity";
+  public static final String DO_NOTHING = "do_nothing";
+  public static final String LAUNCH_DEEP_LINK = "launch_deep_link";
+  public String DEEP_LINK = "";
+  public String ACTIVITY_CLASS = "";
+
+  public String MODE = LAUNCH_DEFAULT_ACTIVITY;
+  // Launch options
+  public String ACTIVITY_EXTRA_FLAGS = "";
+
+  private static final String MOBILE_INSTALL_ATTR = "blaze-mobile-install";
+  private static final String USE_SPLIT_APKS_IF_POSSIBLE = "use-split-apks-if-possible";
+  private static final String INSTANT_RUN_ATTR = "instant-run";
+  private boolean mobileInstall = false;
+  private boolean useSplitApksIfPossible = true;
+  private boolean instantRun = false;
+
+  boolean isMobileInstall() {
+    return mobileInstall;
+  }
+
+  void setMobileInstall(boolean mobileInstall) {
+    this.mobileInstall = mobileInstall;
+  }
+
+  public boolean isUseSplitApksIfPossible() {
+    return useSplitApksIfPossible;
+  }
+
+  void setUseSplitApksIfPossible(boolean useSplitApksIfPossible) {
+    this.useSplitApksIfPossible = useSplitApksIfPossible;
+  }
+
+  boolean isInstantRun() {
+    return instantRun;
+  }
+
+  void setInstantRun(boolean instantRun) {
+    this.instantRun = instantRun;
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    DefaultJDOMExternalizer.readExternal(this, element);
+    setMobileInstall(Boolean.parseBoolean(element.getAttributeValue(MOBILE_INSTALL_ATTR)));
+    setUseSplitApksIfPossible(Boolean.parseBoolean(element.getAttributeValue(USE_SPLIT_APKS_IF_POSSIBLE)));
+    setInstantRun(Boolean.parseBoolean(element.getAttributeValue(INSTANT_RUN_ATTR)));
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    DefaultJDOMExternalizer.writeExternal(this, element);
+    element.setAttribute(MOBILE_INSTALL_ATTR, Boolean.toString(mobileInstall));
+    element.setAttribute(USE_SPLIT_APKS_IF_POSSIBLE, Boolean.toString(useSplitApksIfPossible));
+    element.setAttribute(INSTANT_RUN_ATTR, Boolean.toString(instantRun));
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
new file mode 100644
index 0000000..e85da80
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationStateEditor.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.android.tools.idea.run.ConfigurationSpecificEditor;
+import com.android.tools.idea.run.activity.ActivityLocatorUtils;
+import com.android.tools.idea.run.util.LaunchUtils;
+import com.google.idea.blaze.android.run.binary.instantrun.InstantRunExperiment;
+import com.google.idea.blaze.base.ui.IntegerTextField;
+import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
+import com.intellij.ide.util.TreeClassChooser;
+import com.intellij.ide.util.TreeClassChooserFactory;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.fileTypes.PlainTextLanguage;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ComponentWithBrowseButton;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.Key;
+import com.intellij.psi.JavaPsiFacade;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.ProjectScope;
+import com.intellij.ui.EditorTextField;
+import com.intellij.ui.LanguageTextField;
+import com.intellij.uiDesigner.core.GridConstraints;
+import com.intellij.uiDesigner.core.GridLayoutManager;
+import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.android.util.AndroidUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import javax.swing.border.TitledBorder;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ResourceBundle;
+
+/**
+ * The part of the Blaze Android run configuration editor that allows the user to pick an
+ * android_binary target and an activity to launch.
+ * Patterned after {@link org.jetbrains.android.run.ApplicationRunParameters}.
+ */
+class BlazeAndroidBinaryRunConfigurationStateEditor implements ConfigurationSpecificEditor<BlazeAndroidBinaryRunConfiguration> {
+  public static final Key<BlazeAndroidBinaryRunConfigurationStateEditor> ACTIVITY_CLASS_TEXT_FIELD_KEY =
+    Key.create("BlazeActivityClassTextField");
+
+  @NotNull
+  private final Project project;
+  @Nullable
+  private JPanel panel;
+  private ComponentWithBrowseButton<EditorTextField> activityField;
+  private JRadioButton launchNothingButton;
+  private JRadioButton launchDefaultButton;
+  private JRadioButton launchCustomButton;
+  private JCheckBox mobileInstallCheckBox;
+  private JCheckBox splitApksCheckBox;
+  private JCheckBox instantRunCheckBox;
+  private IntegerTextField userIdField;
+
+  BlazeAndroidBinaryRunConfigurationStateEditor(@NotNull final Project project) {
+    this.project = project;
+
+    setupUI();
+    userIdField.setMinValue(0);
+
+    activityField.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        if (!project.isInitialized()) {
+          return;
+        }
+        // We find all Activity classes in the module for the selected variant (or any of its deps).
+        final JavaPsiFacade facade = JavaPsiFacade.getInstance(project);
+        PsiClass activityBaseClass = facade.findClass(
+          AndroidUtils.ACTIVITY_BASE_CLASS_NAME, ProjectScope.getAllScope(project));
+        if (activityBaseClass == null) {
+          Messages
+            .showErrorDialog(panel, AndroidBundle.message("cant.find.activity.class.error"));
+          return;
+        }
+        GlobalSearchScope searchScope = GlobalSearchScope.projectScope(project);
+        PsiClass initialSelection = facade.findClass(
+          activityField.getChildComponent().getText(), searchScope);
+        TreeClassChooser chooser = TreeClassChooserFactory.getInstance(project)
+          .createInheritanceClassChooser("Select Activity Class",
+                                         searchScope, activityBaseClass,
+                                         initialSelection, null);
+        chooser.showDialog();
+        PsiClass selClass = chooser.getSelected();
+        if (selClass != null) {
+          // This must be done because Android represents inner static class paths differently than java.
+          String qualifiedActivityName = ActivityLocatorUtils.getQualifiedActivityName(selClass);
+          activityField.getChildComponent().setText(qualifiedActivityName);
+        }
+      }
+    });
+    ActionListener listener = e -> activityField.setEnabled(launchCustomButton.isSelected());
+    launchCustomButton.addActionListener(listener);
+    launchDefaultButton.addActionListener(listener);
+    launchNothingButton.addActionListener(listener);
+
+    instantRunCheckBox.setVisible(InstantRunExperiment.INSTANT_RUN_ENABLED.getValue());
+
+    /**
+     * Only one of mobile-install and instant run can be selected at any one time
+     */
+    mobileInstallCheckBox.addActionListener(e -> {
+      if (mobileInstallCheckBox.isSelected()) {
+        instantRunCheckBox.setSelected(false);
+      }
+    });
+    instantRunCheckBox.addActionListener(e -> {
+      if (instantRunCheckBox.isSelected()) {
+        mobileInstallCheckBox.setSelected(false);
+      }
+    });
+
+    mobileInstallCheckBox.addActionListener(e -> splitApksCheckBox.setVisible(mobileInstallCheckBox.isSelected()));
+  }
+
+  @Override
+  public void resetFrom(BlazeAndroidBinaryRunConfiguration configuration) {
+    BlazeAndroidBinaryRunConfigurationState configState = configuration.getConfigState();
+    boolean launchSpecificActivity = configState.MODE.equals(BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY);
+    if (configState.MODE.equals(BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEFAULT_ACTIVITY)) {
+      launchDefaultButton.setSelected(true);
+    }
+    else if (launchSpecificActivity) {
+      launchCustomButton.setSelected(true);
+    }
+    else {
+      launchNothingButton.setSelected(true);
+    }
+    activityField.setEnabled(launchSpecificActivity);
+    if (launchSpecificActivity) {
+      activityField.getChildComponent().setText(configState.ACTIVITY_CLASS);
+    }
+
+    mobileInstallCheckBox.setSelected(configState.isMobileInstall());
+    splitApksCheckBox.setSelected(configState.isUseSplitApksIfPossible());
+    instantRunCheckBox.setSelected(configState.isInstantRun());
+
+    userIdField.setEnabled(!configState.MODE.equals(BlazeAndroidBinaryRunConfigurationState.DO_NOTHING));
+    userIdField.setValue(LaunchUtils.getUserIdFromFlags(configState.ACTIVITY_EXTRA_FLAGS));
+    splitApksCheckBox.setVisible(configState.isMobileInstall());
+  }
+
+  @Override
+  public Component getComponent() {
+    return panel;
+  }
+
+  @Override
+  public void applyTo(BlazeAndroidBinaryRunConfiguration configuration) {
+    BlazeAndroidBinaryRunConfigurationState configState = configuration.getConfigState();
+    configState.ACTIVITY_EXTRA_FLAGS = getFlagsFromUserId((Number)userIdField.getValue());
+    if (launchDefaultButton.isSelected()) {
+      configState.MODE = BlazeAndroidBinaryRunConfigurationState.LAUNCH_DEFAULT_ACTIVITY;
+    }
+    else if (launchCustomButton.isSelected()) {
+      configState.MODE = BlazeAndroidBinaryRunConfigurationState.LAUNCH_SPECIFIC_ACTIVITY;
+      configState.ACTIVITY_CLASS = activityField.getChildComponent().getText();
+    }
+    else {
+      configState.MODE = BlazeAndroidBinaryRunConfigurationState.DO_NOTHING;
+    }
+    configState.setMobileInstall(mobileInstallCheckBox.isSelected());
+    configState.setUseSplitApksIfPossible(splitApksCheckBox.isSelected());
+    configState.setInstantRun(instantRunCheckBox.isSelected());
+  }
+
+  @Override
+  public JComponent getAnchor() {
+    return null;
+  }
+
+  @Override
+  public void setAnchor(JComponent anchor) {
+  }
+
+  private void createUIComponents() {
+    final EditorTextField editorTextField = new LanguageTextField(PlainTextLanguage.INSTANCE,
+                                                                  project, "") {
+      @Override
+      protected EditorEx createEditor() {
+        final EditorEx editor = super.createEditor();
+        final PsiFile file =
+          PsiDocumentManager.getInstance(project).getPsiFile(editor.getDocument());
+
+        if (file != null) {
+          DaemonCodeAnalyzer.getInstance(project).setHighlightingEnabled(file, false);
+        }
+        editor.putUserData(ACTIVITY_CLASS_TEXT_FIELD_KEY, BlazeAndroidBinaryRunConfigurationStateEditor.this);
+        return editor;
+      }
+    };
+    activityField = new ComponentWithBrowseButton<EditorTextField>(editorTextField, null);
+  }
+
+  @NotNull
+  private static String getFlagsFromUserId(@Nullable Number userId) {
+    return userId != null ? ("--user " + userId.intValue()) : "";
+  }
+
+  /**
+   * Initially generated by IntelliJ from a .form file, then checked in as source.
+   */
+  private void setupUI() {
+    createUIComponents();
+    panel = new JPanel();
+    panel.setLayout(new GridLayoutManager(4, 2, new Insets(0, 0, 0, 0), -1, -1));
+    final JPanel panel1 = new JPanel();
+    panel1.setLayout(new GridLayoutManager(4, 2, new Insets(0, 0, 0, 0), -1, -1));
+    panel.add(panel1, new GridConstraints(3, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH,
+                                          GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                          GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0,
+                                          false));
+    panel1.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), "Activity", TitledBorder.DEFAULT_JUSTIFICATION,
+                                                      TitledBorder.DEFAULT_POSITION,
+                                                      new Font(panel1.getFont().getName(), panel1.getFont().getStyle(),
+                                                               panel1.getFont().getSize()), new Color(-16777216)));
+    launchNothingButton = new JRadioButton();
+    this.loadButtonText(launchNothingButton,
+                              ResourceBundle.getBundle("messages/AndroidBundle").getString("android.run.configuration.do.nothing.label"));
+    panel1.add(launchNothingButton, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                                        GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                        GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    launchDefaultButton = new JRadioButton();
+    launchDefaultButton.setText("Launch default Activity");
+    launchDefaultButton.setMnemonic('L');
+    launchDefaultButton.setDisplayedMnemonicIndex(0);
+    panel1.add(launchDefaultButton, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                                        GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                        GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    launchCustomButton = new JRadioButton();
+    launchCustomButton.setText("Launch:");
+    launchCustomButton.setMnemonic('A');
+    launchCustomButton.setDisplayedMnemonicIndex(1);
+    panel1.add(launchCustomButton,
+               new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                   GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    panel1.add(activityField, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH,
+                                                  GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                  GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    final JLabel label1 = new JLabel();
+    label1.setText("User ID");
+    panel1.add(label1,
+               new GridConstraints(3, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                   GridConstraints.SIZEPOLICY_FIXED, null, null, null, 1, false));
+    userIdField = new IntegerTextField();
+    panel1.add(userIdField, new GridConstraints(3, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL,
+                                                GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0,
+                                                false));
+    mobileInstallCheckBox = new JCheckBox();
+    mobileInstallCheckBox.setText(" Use blaze mobile-install (go/as-mi)");
+    panel.add(mobileInstallCheckBox, new GridConstraints(0, 0, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
+                                                         GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                         GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    splitApksCheckBox = new JCheckBox();
+    splitApksCheckBox.setText(" Use --split_apks where possible");
+    panel.add(splitApksCheckBox, new GridConstraints(1, 0, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
+                                                         GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                         GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    instantRunCheckBox = new JCheckBox();
+    instantRunCheckBox.setText(" Use InstantRun");
+    panel.add(instantRunCheckBox, new GridConstraints(2, 0, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
+                                                         GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                         GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    ButtonGroup buttonGroup;
+    buttonGroup = new ButtonGroup();
+    buttonGroup.add(launchDefaultButton);
+    buttonGroup.add(launchCustomButton);
+    buttonGroup.add(launchNothingButton);
+  }
+
+  /**
+   * Initially generated by IntelliJ from a .form file.
+   */
+  private void loadButtonText(AbstractButton component, String text) {
+    StringBuffer result = new StringBuffer();
+    boolean haveMnemonic = false;
+    char mnemonic = '\0';
+    int mnemonicIndex = -1;
+    for (int i = 0; i < text.length(); i++) {
+      if (text.charAt(i) == '&') {
+        i++;
+        if (i == text.length()) break;
+        if (!haveMnemonic && text.charAt(i) != '&') {
+          haveMnemonic = true;
+          mnemonic = text.charAt(i);
+          mnemonicIndex = result.length();
+        }
+      }
+      result.append(text.charAt(i));
+    }
+    component.setText(result.toString());
+    if (haveMnemonic) {
+      component.setMnemonic(mnemonic);
+      component.setDisplayedMnemonicIndex(mnemonicIndex);
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationType.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationType.java
new file mode 100644
index 0000000..d4f9e6a
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeAndroidBinaryRunConfigurationType.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.google.idea.blaze.android.run.BlazeBeforeRunTaskProvider;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.execution.BeforeRunTask;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.ConfigurationType;
+import com.intellij.execution.configurations.ConfigurationTypeUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import icons.AndroidIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+/**
+ * A type for Android application run configurations adapted specifically to run android_binary
+ * targets.
+ */
+public class BlazeAndroidBinaryRunConfigurationType implements ConfigurationType {
+  private final BlazeAndroidBinaryRunConfigurationFactory factory =
+    new BlazeAndroidBinaryRunConfigurationFactory(this);
+
+  public static class BlazeAndroidBinaryRuleConfigurationFactory implements BlazeRuleConfigurationFactory {
+    @Override
+    public boolean handlesRule(WorkspaceLanguageSettings workspaceLanguageSettings, @NotNull RuleIdeInfo rule) {
+      return rule.kindIsOneOf(Kind.ANDROID_BINARY);
+    }
+
+    @Override
+    @NotNull
+    public RunnerAndConfigurationSettings createForRule(@NotNull RunManager runManager, @NotNull RuleIdeInfo rule) {
+      return getInstance().factory.createForRule(runManager, rule);
+    }
+  }
+
+  public static class BlazeAndroidBinaryRunConfigurationFactory
+    extends ConfigurationFactory {
+
+    protected BlazeAndroidBinaryRunConfigurationFactory(@NotNull ConfigurationType type) {
+      super(type);
+    }
+
+    @Override
+    @NotNull
+    public BlazeAndroidBinaryRunConfiguration createTemplateConfiguration(@NotNull Project project) {
+      return new BlazeAndroidBinaryRunConfiguration(project, this);
+    }
+
+    @Override
+    public boolean canConfigurationBeSingleton() {
+      return false;
+    }
+
+    @Override
+    public boolean isApplicable(@NotNull Project project) {
+      return Blaze.isBlazeProject(project);
+    }
+
+    @Override
+    public void configureBeforeRunTaskDefaults(
+      Key<? extends BeforeRunTask> providerID, BeforeRunTask task) {
+      task.setEnabled(providerID.equals(BlazeBeforeRunTaskProvider.ID));
+    }
+
+    @NotNull
+    public RunnerAndConfigurationSettings createForRule(@NotNull RunManager runManager, @NotNull RuleIdeInfo rule) {
+      final RunnerAndConfigurationSettings settings =
+        runManager.createRunConfiguration(rule.label.toString(), this);
+      final BlazeAndroidBinaryRunConfiguration configuration =
+        (BlazeAndroidBinaryRunConfiguration) settings.getConfiguration();
+      configuration.setTarget(rule.label);
+      return settings;
+    }
+
+    @Override
+    public boolean isConfigurationSingletonByDefault() {
+      return false;
+    }
+  }
+
+  @NotNull
+  public static BlazeAndroidBinaryRunConfigurationType getInstance() {
+    return
+      ConfigurationTypeUtil.findConfigurationType(BlazeAndroidBinaryRunConfigurationType.class);
+  }
+
+  @Override
+  @NotNull
+  public String getDisplayName() {
+    return Blaze.defaultBuildSystemName() + " Android Binary";
+  }
+
+  @Override
+  public String getConfigurationTypeDescription() {
+    return "Launch/debug configuration for android_binary rules";
+  }
+
+  @Override
+  public Icon getIcon() {
+    return AndroidIcons.Android;
+  }
+
+  @Override
+  @NotNull
+  public String getId() {
+    return "BlazeAndroidBinaryRunConfigurationType";
+  }
+
+  @Override
+  public BlazeAndroidBinaryRunConfigurationFactory[] getConfigurationFactories() {
+    return new BlazeAndroidBinaryRunConfigurationFactory[]{factory};
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/BlazeDefaultActivityLocator.java b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeDefaultActivityLocator.java
new file mode 100644
index 0000000..0891fb8
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/BlazeDefaultActivityLocator.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.activity.ActivityLocator;
+import com.android.tools.idea.run.activity.DefaultActivityLocator;
+import com.google.idea.blaze.android.manifest.ManifestParser;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
+import org.jetbrains.android.dom.manifest.Manifest;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * An activity launcher which extracts the default launch activity from a generated APK and starts
+ * it.
+ */
+public class BlazeDefaultActivityLocator extends ActivityLocator {
+  private final Project project;
+  private final File mergedManifestFile;
+
+  public BlazeDefaultActivityLocator(
+    Project project,
+    File mergedManifestFile
+  ) {
+    this.project = project;
+    this.mergedManifestFile = mergedManifestFile;
+  }
+
+  @Override
+  public void validate() throws ActivityLocatorException {
+  }
+
+  @NotNull
+  @Override
+  public String getQualifiedActivityName(@NotNull IDevice device) throws ActivityLocatorException {
+    Manifest manifest = ManifestParser.getInstance(project).getManifest(mergedManifestFile);
+    if (manifest == null) {
+      throw new ActivityLocatorException("Could not locate merged manifest");
+    }
+    String activityName = ApplicationManager.getApplication().runReadAction(
+      (Computable<String>)() -> DefaultActivityLocator.getDefaultLauncherActivityName(project, manifest)
+    );
+    if (activityName == null) {
+      throw new ActivityLocatorException("Could not locate default activity to launch.");
+    }
+    return activityName;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeAndroidBinaryInstantRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeAndroidBinaryInstantRunContext.java
new file mode 100644
index 0000000..4d260c0
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeAndroidBinaryInstantRunContext.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.android.run.binary.instantrun;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.fd.InstantRunBuildAnalyzer;
+import com.android.tools.idea.fd.InstantRunUtils;
+import com.android.tools.idea.run.ApplicationIdProvider;
+import com.android.tools.idea.run.ConsoleProvider;
+import com.android.tools.idea.run.LaunchOptions;
+import com.android.tools.idea.run.activity.DefaultStartActivityFlagsProvider;
+import com.android.tools.idea.run.activity.StartActivityFlagsProvider;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.tasks.DebugConnectorTask;
+import com.android.tools.idea.run.tasks.LaunchTask;
+import com.android.tools.idea.run.tasks.LaunchTasksProvider;
+import com.android.tools.idea.run.tasks.UpdateSessionTasksProvider;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryApplicationLaunchTaskProvider;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryConsoleProvider;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationState;
+import com.google.idea.blaze.android.run.runner.*;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.util.Set;
+
+/**
+ * Run context for InstantRun.
+ */
+public class BlazeAndroidBinaryInstantRunContext implements BlazeAndroidRunContext {
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidBinaryInstantRunContext.class);
+
+  private final Project project;
+  private final AndroidFacet facet;
+  private final RunConfiguration runConfiguration;
+  private final ExecutionEnvironment env;
+  private final BlazeAndroidBinaryRunConfigurationState configState;
+
+  private final BlazeAndroidBinaryConsoleProvider consoleProvider;
+  private final BlazeApkBuildStepInstantRun buildStep;
+
+  public BlazeAndroidBinaryInstantRunContext(Project project,
+                                             AndroidFacet facet,
+                                             RunConfiguration runConfiguration,
+                                             ExecutionEnvironment env,
+                                             BlazeAndroidRunConfigurationCommonState commonState,
+                                             BlazeAndroidBinaryRunConfigurationState configState,
+                                             ImmutableList<String> buildFlags) {
+    this.project = project;
+    this.facet = facet;
+    this.runConfiguration = runConfiguration;
+    this.env = env;
+    this.configState = configState;
+    this.consoleProvider = new BlazeAndroidBinaryConsoleProvider(project);
+    this.buildStep = new BlazeApkBuildStepInstantRun(project, env, commonState, buildFlags);
+  }
+
+  @Override
+  public BlazeAndroidDeviceSelector getDeviceSelector() {
+    return new BlazeInstantRunDeviceSelector();
+  }
+
+  @Override
+  public void augmentEnvironment(ExecutionEnvironment env) {
+    InstantRunUtils.setInstantRunEnabled(env, true);
+  }
+
+  @Override
+  public void augmentLaunchOptions(@NotNull LaunchOptions.Builder options) {
+    options
+      .setDeploy(true)
+      .setPmInstallOptions(configState.ACTIVITY_EXTRA_FLAGS)
+      .setOpenLogcatAutomatically(true);
+  }
+
+  @NotNull
+  @Override
+  public ConsoleProvider getConsoleProvider() {
+    return consoleProvider;
+  }
+
+  @Override
+  public ApplicationIdProvider getApplicationIdProvider() throws ExecutionException {
+    return Futures.get(buildStep.getApplicationIdProvider(), ExecutionException.class);
+  }
+
+  @Override
+  public BlazeApkBuildStep getBuildStep() {
+    return buildStep;
+  }
+
+  @Override
+  public LaunchTasksProvider getLaunchTasksProvider(
+    LaunchOptions launchOptions,
+    BlazeAndroidRunConfigurationDebuggerManager debuggerManager) throws ExecutionException {
+    InstantRunBuildAnalyzer analyzer = Futures.get(buildStep.getInstantRunBuildAnalyzer(), ExecutionException.class);
+
+    if (analyzer.canReuseProcessHandler()) {
+      return new UpdateSessionTasksProvider(analyzer);
+    }
+    return new BlazeAndroidLaunchTasksProvider(project, this, getApplicationIdProvider(), launchOptions, debuggerManager);
+  }
+
+  @Override
+  public ImmutableList<LaunchTask> getDeployTasks(IDevice device, LaunchOptions launchOptions) throws ExecutionException {
+    InstantRunBuildAnalyzer analyzer = Futures.get(buildStep.getInstantRunBuildAnalyzer(), ExecutionException.class);
+    return ImmutableList.<LaunchTask>builder()
+      .addAll(analyzer.getDeployTasks(launchOptions))
+      .add(analyzer.getNotificationTask())
+      .build();
+  }
+
+  @Nullable
+  @Override
+  public LaunchTask getApplicationLaunchTask(LaunchOptions launchOptions,
+                                             AndroidDebugger androidDebugger,
+                                             AndroidDebuggerState androidDebuggerState,
+                                             ProcessHandlerLaunchStatus processHandlerLaunchStatus) throws ExecutionException {
+    BlazeApkBuildStepInstantRun.BuildResult buildResult = Futures.get(buildStep.getBuildResult(), ExecutionException.class);
+
+    final StartActivityFlagsProvider startActivityFlagsProvider = new DefaultStartActivityFlagsProvider(
+      androidDebugger,
+      androidDebuggerState,
+      project,
+      launchOptions.isDebug(),
+      configState.ACTIVITY_EXTRA_FLAGS
+    );
+
+    ApplicationIdProvider applicationIdProvider = getApplicationIdProvider();
+    return BlazeAndroidBinaryApplicationLaunchTaskProvider.getApplicationLaunchTask(
+      project,
+      applicationIdProvider,
+      buildResult.mergedManifestFile,
+      configState,
+      startActivityFlagsProvider,
+      processHandlerLaunchStatus
+    );
+  }
+
+  @Nullable
+  @Override
+  public DebugConnectorTask getDebuggerTask(LaunchOptions launchOptions,
+                                            AndroidDebugger androidDebugger,
+                                            AndroidDebuggerState androidDebuggerState,
+                                            Set<String> packageIds) throws ExecutionException {
+    //noinspection unchecked
+    return androidDebugger.getConnectDebuggerTask(env, null, packageIds, facet, androidDebuggerState, runConfiguration.getType().getId());
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeApkBuildStepInstantRun.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeApkBuildStepInstantRun.java
new file mode 100644
index 0000000..46ca755
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeApkBuildStepInstantRun.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.instantrun;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.fd.*;
+import com.android.tools.idea.gradle.run.MakeBeforeRunTaskProvider;
+import com.android.tools.idea.run.*;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.idea.blaze.android.manifest.ManifestParser;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
+import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.google.repackaged.devtools.build.lib.rules.android.apkmanifest.ApkManifestOuterClass;
+import com.intellij.execution.Executor;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.io.*;
+import java.lang.reflect.InvocationTargetException;
+import java.util.List;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Builds the APK using normal blaze build.
+ */
+class BlazeApkBuildStepInstantRun implements BlazeApkBuildStep {
+  private static final Logger LOG = Logger.getInstance(BlazeApkBuildStepInstantRun.class);
+
+  private final Project project;
+  private final Executor executor;
+  private final ExecutionEnvironment env;
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+  private final ImmutableList<String> buildFlags;
+  private final File instantRunArtifactDirectory;
+  private final File instantRunGradleBuildFile;
+  private final File instantRunBuildInfoFile;
+  private final File instantRunGradlePropertiesFile;
+
+
+  public static class BuildResult {
+    public final File executionRoot;
+    public final File mergedManifestFile;
+    public final File apkManifestProtoFile;
+    public final ApkManifestOuterClass.ApkManifest apkManifestProto;
+    public BuildResult(File executionRoot,
+                       File mergedManifestFile,
+                       File apkManifestProtoFile,
+                       ApkManifestOuterClass.ApkManifest apkManifestProto) {
+      this.executionRoot = executionRoot;
+      this.mergedManifestFile = mergedManifestFile;
+      this.apkManifestProtoFile = apkManifestProtoFile;
+      this.apkManifestProto = apkManifestProto;
+    }
+  }
+  private final SettableFuture<BuildResult> buildResultFuture = SettableFuture.create();
+  private final SettableFuture<ApplicationIdProvider> applicationIdProviderFuture = SettableFuture.create();
+  private final SettableFuture<InstantRunContext> instantRunContextFuture = SettableFuture.create();
+  private final SettableFuture<InstantRunBuildAnalyzer> instantRunBuildAnalyzerFuture = SettableFuture.create();
+
+  public BlazeApkBuildStepInstantRun(Project project,
+                                     ExecutionEnvironment env,
+                                     BlazeAndroidRunConfigurationCommonState commonState,
+                                     ImmutableList<String> buildFlags) {
+    this.project = project;
+    this.executor = env.getExecutor();
+    this.env = env;
+    this.commonState = commonState;
+    this.buildFlags = buildFlags;
+    this.instantRunArtifactDirectory = BlazeInstantRunGradleIntegration.getInstantRunArtifactDirectory(project, commonState.getTarget());
+    this.instantRunBuildInfoFile = new File(instantRunArtifactDirectory, "build/reload-dex/debug/build-info.xml");
+    this.instantRunGradleBuildFile = new File(instantRunArtifactDirectory, "build.gradle");
+    this.instantRunGradlePropertiesFile = new File(instantRunArtifactDirectory, "gradle.properties");
+  }
+
+  @Override
+  public boolean build(BlazeContext context, BlazeAndroidDeviceSelector.DeviceSession deviceSession) {
+    if (!instantRunArtifactDirectory.exists() && !instantRunArtifactDirectory.mkdirs()) {
+      IssueOutput.error("Could not create instant run artifact directory: " + instantRunArtifactDirectory).submit(context);
+      return false;
+    }
+
+    BuildResult buildResult = buildApkManifest(context);
+    if (buildResult == null) {
+      return false;
+    }
+
+    String gradleUrl = BlazeInstantRunGradleIntegration.getGradleUrl(context);
+    if (gradleUrl == null) {
+      return false;
+    }
+
+    ApplicationIdProvider applicationIdProvider = new BlazeInstantRunApplicationIdProvider(project, buildResult);
+    applicationIdProviderFuture.set(applicationIdProvider);
+
+    // Write build.gradle
+    try (PrintWriter printWriter = new PrintWriter(instantRunGradleBuildFile)) {
+      printWriter.print(BlazeInstantRunGradleIntegration.getGradleBuildInfoString(
+        gradleUrl,
+        buildResult.executionRoot,
+        buildResult.apkManifestProtoFile
+      ));
+    }
+    catch (IOException e) {
+      IssueOutput.error("Could not write build.gradle file: " + e).submit(context);
+      return false;
+    }
+
+    // Write gradle.properties
+    try (PrintWriter printWriter = new PrintWriter(instantRunGradlePropertiesFile)) {
+      printWriter.print(BlazeInstantRunGradleIntegration.getGradlePropertiesString());
+    }
+    catch (IOException e) {
+      IssueOutput.error("Could not write build.gradle file: " + e).submit(context);
+      return false;
+    }
+
+    String applicationId = null;
+    try {
+      applicationId = applicationIdProvider.getPackageName();
+    }
+    catch (ApkProvisionException e) {
+      return false;
+    }
+
+    return invokeGradleIrTasks(context, deviceSession, buildResult, applicationId);
+  }
+
+  private BuildResult buildApkManifest(BlazeContext context) {
+    final ScopedTask buildTask = new ScopedTask(context) {
+      @Override
+      protected void execute(@NotNull BlazeContext context) {
+        WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+        String executionRoot = getExecutionRoot(context, workspaceRoot);
+        if (executionRoot == null) {
+          IssueOutput.error("Could not get execution root").submit(context);
+          return;
+        }
+
+        BlazeCommand.Builder command = BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD);
+
+        command
+          .addTargets(commonState.getTarget())
+          .addBlazeFlags(buildFlags)
+          .addBlazeFlags("--output_groups=apk_manifest")
+          .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS)
+        ;
+
+        List<File> apkManifestFiles = Lists.newArrayList();
+
+        SaveUtil.saveAllFiles();
+        int retVal = ExternalTask.builder(workspaceRoot, command.build())
+          .context(context)
+          .stderr(LineProcessingOutputStream.of(
+            new ExperimentalShowArtifactsLineProcessor(apkManifestFiles, "apk_manifest"),
+            new IssueOutputLineProcessor(project, context, workspaceRoot)
+          ))
+          .build()
+          .run(new LoggedTimingScope(project, Action.BLAZE_BUILD));
+        LocalFileSystem.getInstance().refresh(true);
+
+        if (retVal != 0) {
+          context.setHasError();
+          return;
+        }
+
+        File apkManifestFile = Iterables.getOnlyElement(apkManifestFiles, null);
+        if (apkManifestFile == null) {
+          IssueOutput.error("Could not find APK manifest file").submit(context);
+          return;
+        }
+
+        ApkManifestOuterClass.ApkManifest apkManifestProto;
+        try (InputStream inputStream = new FileInputStream(apkManifestFile)) {
+          apkManifestProto = ApkManifestOuterClass.ApkManifest.parseFrom(inputStream);
+        }
+        catch (IOException e) {
+          LOG.error(e);
+          IssueOutput.error("Error parsing apk proto").submit(context);
+          return;
+        }
+
+        // Refresh the manifest
+        File mergedManifestFile = new File(executionRoot, apkManifestProto.getAndroidManifest().getExecRootPath());
+        ManifestParser.getInstance(project).refreshManifests(ImmutableList.of(mergedManifestFile));
+
+        BuildResult buildResult = new BuildResult(
+          new File(executionRoot),
+          mergedManifestFile,
+          apkManifestFile,
+          apkManifestProto
+        );
+        buildResultFuture.set(buildResult);
+      }
+    };
+
+    BlazeExecutor.submitTask(
+      project,
+      String.format("Executing %s apk build", Blaze.buildSystemName(project)),
+      buildTask
+    );
+
+    try {
+      BuildResult buildResult = buildResultFuture.get();
+      if (!context.shouldContinue()) {
+        return null;
+      }
+      return buildResult;
+    }
+    catch (InterruptedException|ExecutionException e) {
+      context.setHasError();
+    }
+    catch (CancellationException e) {
+      context.setCancelled();
+    }
+    return null;
+  }
+
+  private boolean invokeGradleIrTasks(BlazeContext context,
+                                      BlazeAndroidDeviceSelector.DeviceSession deviceSession,
+                                      BuildResult buildResult,
+                                      String applicationId) {
+    InstantRunContext instantRunContext = new BlazeInstantRunContext(
+      project,
+      buildResult.apkManifestProto,
+      applicationId,
+      instantRunBuildInfoFile
+    );
+    instantRunContextFuture.set(instantRunContext);
+    ProcessHandler previousSessionProcessHandler = deviceSession.sessionInfo != null
+                                                   ? deviceSession.sessionInfo.getProcessHandler()
+                                                   : null;
+    DeviceFutures deviceFutures = deviceSession.deviceFutures;
+    assert deviceFutures != null;
+    List<AndroidDevice> targetDevices = deviceFutures.getDevices();
+    AndroidDevice androidDevice = targetDevices.get(0);
+    IDevice device = getLaunchedDevice(androidDevice);
+
+    AndroidRunConfigContext runConfigContext = new AndroidRunConfigContext();
+    runConfigContext.setTargetDevices(deviceFutures);
+
+    AndroidSessionInfo info = deviceSession.sessionInfo;
+    runConfigContext.setSameExecutorAsPreviousSession(info != null && executor.getId().equals(info.getExecutorId()));
+    runConfigContext.setCleanRerun(InstantRunUtils.isCleanReRun(env));
+
+    InstantRunBuilder instantRunBuilder = new InstantRunBuilder(
+      device,
+      instantRunContext,
+      runConfigContext,
+      new BlazeInstantRunTasksProvider(),
+      RunAsValidityService.getInstance()
+    );
+
+    try {
+      List<String> cmdLineArgs = Lists.newArrayList();
+      cmdLineArgs.addAll(MakeBeforeRunTaskProvider.getDeviceSpecificArguments(targetDevices));
+      BlazeInstantRunGradleTaskRunner taskRunner = new BlazeInstantRunGradleTaskRunner(project, context, instantRunGradleBuildFile);
+      boolean success = instantRunBuilder.build(taskRunner, cmdLineArgs);
+      LOG.info("Gradle invocation complete, success = " + success);
+      if (!success) {
+        return false;
+      }
+    }
+    catch (InvocationTargetException e) {
+      LOG.info("Unexpected error while launching gradle before run tasks", e);
+      return false;
+    }
+    catch (InterruptedException e) {
+      LOG.info("Interrupted while launching gradle before run tasks");
+      Thread.currentThread().interrupt();
+      return false;
+    }
+
+    InstantRunBuildAnalyzer analyzer = new InstantRunBuildAnalyzer(
+      project,
+      instantRunContext,
+      previousSessionProcessHandler
+    );
+    instantRunBuildAnalyzerFuture.set(analyzer);
+    return true;
+  }
+
+  ListenableFuture<BuildResult> getBuildResult() {
+    return buildResultFuture;
+  }
+
+  ListenableFuture<ApplicationIdProvider> getApplicationIdProvider() {
+    return applicationIdProviderFuture;
+  }
+
+  ListenableFuture<InstantRunContext> getInstantRunContext() {
+    return instantRunContextFuture;
+  }
+
+  ListenableFuture<InstantRunBuildAnalyzer> getInstantRunBuildAnalyzer() {
+    return instantRunBuildAnalyzerFuture;
+  }
+
+  private String getExecutionRoot(BlazeContext context, WorkspaceRoot workspaceRoot) {
+    ListenableFuture<String> execRootFuture = BlazeInfo.getInstance().runBlazeInfo(
+      context, Blaze.getBuildSystem(project),
+      workspaceRoot,
+      buildFlags,
+      BlazeInfo.EXECUTION_ROOT_KEY
+    );
+    try {
+      return execRootFuture.get();
+    }
+    catch (InterruptedException e) {
+      context.setCancelled();
+    }
+    catch (ExecutionException e) {
+      LOG.error(e);
+      context.setHasError();
+    }
+    return null;
+  }
+
+  @Nullable
+  private static IDevice getLaunchedDevice(@NotNull AndroidDevice device) {
+    if (!device.getLaunchedDevice().isDone()) {
+      // If we don't have access to the device (this happens if the AVD is still launching)
+      return null;
+    }
+
+    try {
+      return device.getLaunchedDevice().get(1, TimeUnit.MILLISECONDS);
+    }
+    catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      return null;
+    }
+    catch (ExecutionException | TimeoutException e) {
+      return null;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunApplicationIdProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunApplicationIdProvider.java
new file mode 100644
index 0000000..7349986
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunApplicationIdProvider.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.instantrun;
+
+import com.android.tools.idea.run.ApkProvisionException;
+import com.android.tools.idea.run.ApplicationIdProvider;
+import com.google.idea.blaze.android.manifest.ManifestParser;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
+import org.jetbrains.android.dom.manifest.Manifest;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+/**
+ * Application id provider for blaze instant run.
+ */
+public class BlazeInstantRunApplicationIdProvider implements ApplicationIdProvider {
+  private final Project project;
+  private final BlazeApkBuildStepInstantRun.BuildResult buildResult;
+
+  public BlazeInstantRunApplicationIdProvider(Project project,
+                                              BlazeApkBuildStepInstantRun.BuildResult buildResult) {
+    this.project = project;
+    this.buildResult = buildResult;
+  }
+
+  @NotNull
+  @Override
+  public String getPackageName() throws ApkProvisionException {
+    File manifestFile = new File(buildResult.executionRoot, buildResult.apkManifestProto.getAndroidManifest().getExecRootPath());
+    Manifest manifest = ManifestParser.getInstance(project).getManifest(manifestFile);
+    if (manifest == null) {
+      throw new ApkProvisionException("Could not find merged manifest: " + manifestFile);
+    }
+    String applicationId = ApplicationManager.getApplication().runReadAction(
+      (Computable<String>)() -> manifest.getPackage().getValue()
+    );
+    if (applicationId == null) {
+      throw new ApkProvisionException("No application id in merged manifest: " + manifestFile);
+    }
+    return applicationId;
+  }
+
+  @Nullable
+  @Override
+  public String getTestPackageName() throws ApkProvisionException {
+    return null;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunContext.java
new file mode 100644
index 0000000..2d86199
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunContext.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.instantrun;
+
+import com.android.tools.fd.client.InstantRunBuildInfo;
+import com.android.tools.idea.fd.BuildSelection;
+import com.android.tools.idea.fd.FileChangeListener;
+import com.android.tools.idea.fd.InstantRunContext;
+import com.google.common.hash.HashCode;
+import com.google.repackaged.devtools.build.lib.rules.android.apkmanifest.ApkManifestOuterClass;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * Blaze implementation of instant run context.
+ */
+public class BlazeInstantRunContext implements InstantRunContext {
+  private static final Logger LOG = Logger.getInstance(BlazeInstantRunContext.class);
+  private final Project project;
+  private final ApkManifestOuterClass.ApkManifest apkManifest;
+  private final String applicationId;
+  private final File instantRunBuildInfoFile;
+  private BuildSelection buildSelection;
+
+  BlazeInstantRunContext(Project project,
+                         ApkManifestOuterClass.ApkManifest apkManifest,
+                         String applicationId,
+                         File instantRunBuildInfoFile) {
+    this.project = project;
+    this.apkManifest = apkManifest;
+    this.applicationId = applicationId;
+    this.instantRunBuildInfoFile = instantRunBuildInfoFile;
+  }
+
+  @NotNull
+  @Override
+  public String getApplicationId() {
+    return applicationId;
+  }
+
+  @NotNull
+  @Override
+  public HashCode getManifestResourcesHash() {
+    // TODO b/28373160
+    return HashCode.fromInt(0);
+  }
+
+  @Override
+  public boolean usesMultipleProcesses() {
+    // TODO(tomlu) -- does this make sense in blaze? We can of course just parse the manifest.
+    return false;
+  }
+
+  @Nullable
+  @Override
+  public FileChangeListener.Changes getFileChangesAndReset() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public InstantRunBuildInfo getInstantRunBuildInfo() {
+    if (instantRunBuildInfoFile.exists()) {
+      try {
+        String xml = new String(Files.readAllBytes(Paths.get(instantRunBuildInfoFile.getPath())), StandardCharsets.UTF_8);
+        return InstantRunBuildInfo.get(xml);
+      }
+      catch (IOException e) {
+        LOG.error(e);
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public void setBuildSelection(@NotNull BuildSelection buildSelection) {
+    this.buildSelection = buildSelection;
+  }
+
+  @Override
+  public BuildSelection getBuildSelection() {
+    return buildSelection;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunDeviceSelector.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunDeviceSelector.java
new file mode 100644
index 0000000..0bebd20
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunDeviceSelector.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.instantrun;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.fd.InstantRunManager;
+import com.android.tools.idea.fd.InstantRunUtils;
+import com.android.tools.idea.run.AndroidSessionInfo;
+import com.android.tools.idea.run.DeviceFutures;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationDeployTargetManager;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.facet.AndroidFacet;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Tries to reuse devices from a previous session.
+ */
+public class BlazeInstantRunDeviceSelector implements BlazeAndroidDeviceSelector {
+  NormalDeviceSelector normalDeviceSelector = new NormalDeviceSelector();
+
+  @Override
+  public DeviceSession getDevice(Project project,
+                                 AndroidFacet facet,
+                                 BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager,
+                                 Executor executor,
+                                 ExecutionEnvironment env,
+                                 AndroidSessionInfo info,
+                                 boolean debug,
+                                 int runConfigId) throws ExecutionException {
+    DeviceFutures deviceFutures = null;
+    if (info != null) {
+      // if there is an existing previous session, then see if we can detect devices to fast deploy to
+      deviceFutures = getFastDeployDevices(executor, info);
+
+      if (InstantRunUtils.isReRun(env)) {
+        info.getProcessHandler().destroyProcess();
+        info = null;
+      }
+    }
+
+    if (deviceFutures != null) {
+      return new DeviceSession(null, deviceFutures, info);
+    }
+
+    // Fall back to normal device selection
+    return normalDeviceSelector.getDevice(project, facet, deployTargetManager, executor, env, info, debug, runConfigId);
+  }
+
+  @Nullable
+  private static DeviceFutures getFastDeployDevices(Executor executor,
+                                                    AndroidSessionInfo info) {
+    if (!info.getExecutorId().equals(executor.getId())) {
+      String msg = String.format("Cannot Instant Run since old executor (%1$s) doesn't match current executor (%2$s)", info.getExecutorId(),
+                                 executor.getId());
+      InstantRunManager.LOG.info(msg);
+      return null;
+    }
+
+    List<IDevice> devices = info.getDevices();
+    if (devices == null || devices.isEmpty()) {
+      InstantRunManager.LOG.info("Cannot Instant Run since we could not locate the devices from the existing launch session");
+      return null;
+    }
+
+    if (devices.size() > 1) {
+      InstantRunManager.LOG.info("Last run was on > 1 device, not reusing devices and prompting again");
+      return null;
+    }
+
+    return DeviceFutures.forDevices(devices);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleIntegration.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleIntegration.java
new file mode 100644
index 0000000..c95f42e
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleIntegration.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.android.run.binary.instantrun;
+
+import com.android.SdkConstants;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.hash.Hashing;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.async.process.PrintOutputLineProcessor;
+import com.google.idea.blaze.base.experiments.DeveloperFlag;
+import com.google.idea.blaze.base.experiments.StringExperiment;
+import com.google.idea.blaze.base.model.primitives.Label;
+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.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.projectstructure.ModuleDataStorage;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * Defines where instant run storage and artifacts go.
+ */
+class BlazeInstantRunGradleIntegration {
+  private static final String INSTANT_RUN_SUBDIRECTORY = "instantrun";
+
+  private static StringExperiment LOCAL_GRADLE_VERSION = new StringExperiment("use.local.gradle.version");
+  private static DeveloperFlag REBUILD_LOCAL_GRADLE = new DeveloperFlag("rebuild.local.gradle");
+
+  /**
+   * Gets a unique directory for a given target that can be used for the build process.
+   */
+  static File getInstantRunArtifactDirectory(Project project, Label target) {
+    BlazeImportSettings importSettings = BlazeImportSettingsManager.getInstance(project).getImportSettings();
+    assert importSettings != null;
+    File dataSubDirectory = new File(importSettings.getProjectDataDirectory(), ModuleDataStorage.DATA_SUBDIRECTORY);
+    File instantRunDirectory = new File(dataSubDirectory, INSTANT_RUN_SUBDIRECTORY);
+    String targetHash = Hashing.md5().hashUnencodedChars(target.toString()).toString();
+    return new File(instantRunDirectory, targetHash);
+  }
+
+  @Nullable
+  static String getGradleUrl(BlazeContext context) {
+    String localGradleVersion = LOCAL_GRADLE_VERSION.getValue();
+    boolean isDevMode = localGradleVersion != null;
+
+    if (isDevMode) {
+      String toolsIdeaPath = PathManager.getHomePath();
+      File toolsDir = new File(toolsIdeaPath).getParentFile();
+      File repoDir = toolsDir.getParentFile();
+      File localGradleDirectory = new File(new File(repoDir, "out/repo/com/android/tools/build/builder"), localGradleVersion);
+      if (REBUILD_LOCAL_GRADLE.getValue() || !localGradleDirectory.exists()) {
+        // Build gradle
+        context.output(PrintOutput.output("Building local Gradle..."));
+        int retVal = ExternalTask.builder(toolsDir, ImmutableList.of("./gradlew", ":init", ":publishLocal"))
+          .stdout(LineProcessingOutputStream.of(new PrintOutputLineProcessor(context)))
+          .build()
+          .run();
+
+        if (retVal != 0) {
+          IssueOutput.error("Gradle build failed.").submit(context);
+          return null;
+        }
+      }
+      return new File(repoDir, "out/repo").getPath();
+    }
+
+    // Not supported yet
+    IssueOutput.error("You must specify 'use.local.gradle.version' experiment, non-local gradle not supported yet.").submit(context);
+    return null;
+  }
+
+  static String getGradlePropertiesString() {
+    return Joiner.on('\n').join(
+      "org.gradle.daemon=true",
+      "org.gradle.jvmargs=-XX:MaxPermSize=1024m -Xmx4096m"
+    );
+  }
+
+  static String getGradleBuildInfoString(String gradleUrl, File executionRoot, File apkManifestFile) {
+    String template = Joiner.on('\n').join(
+      "buildscript {",
+      "  repositories {",
+      "    jcenter()",
+      "    maven { url '%s' }",
+      "  }",
+      "  dependencies {",
+      "    classpath 'com.android.tools.build:gradle:%s'",
+      "  }",
+      "}",
+      "apply plugin: 'com.android.external.build'",
+      "externalBuild {",
+      "  executionRoot = '%s'",
+      "  buildManifestPath = '%s'",
+      "}"
+    );
+    String gradleVersion = LOCAL_GRADLE_VERSION.getValue();
+    gradleVersion = gradleVersion != null ? gradleVersion : SdkConstants.GRADLE_LATEST_VERSION;
+
+    return String.format(template,
+                         gradleUrl,
+                         gradleVersion,
+                         executionRoot.getPath(),
+                         apkManifestFile.getPath()
+    );
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleTaskRunner.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleTaskRunner.java
new file mode 100644
index 0000000..51554b2
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunGradleTaskRunner.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.instantrun;
+
+import com.android.builder.model.AndroidProject;
+import com.android.tools.idea.gradle.invoker.GradleInvocationResult;
+import com.android.tools.idea.gradle.invoker.GradleInvoker;
+import com.android.tools.idea.gradle.run.GradleTaskRunner;
+import com.android.tools.idea.gradle.util.AndroidGradleSettings;
+import com.android.tools.idea.gradle.util.BuildMode;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskId;
+import com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskNotificationListenerAdapter;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.concurrency.Semaphore;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.io.File;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static com.android.tools.idea.gradle.util.GradleUtil.GRADLE_SYSTEM_ID;
+import static com.intellij.openapi.externalSystem.model.task.ExternalSystemTaskType.EXECUTE_TASK;
+
+public class BlazeInstantRunGradleTaskRunner implements GradleTaskRunner {
+  private final Project project;
+  private final BlazeContext context;
+  private final File instantRunGradleBuildFile;
+
+  public BlazeInstantRunGradleTaskRunner(Project project, BlazeContext context, File instantRunGradleBuildFile) {
+    this.project = project;
+    this.context = context;
+    this.instantRunGradleBuildFile = instantRunGradleBuildFile;
+  }
+
+  @Override
+  public boolean run(@NotNull List<String> tasks, @Nullable BuildMode buildMode, @NotNull List<String> commandLineArguments)
+    throws InvocationTargetException, InterruptedException {
+    assert !ApplicationManager.getApplication().isDispatchThread();
+
+    final GradleInvoker gradleInvoker = GradleInvoker.getInstance(project);
+
+    final AtomicBoolean success = new AtomicBoolean();
+    final Semaphore done = new Semaphore();
+    done.down();
+
+    final GradleInvoker.AfterGradleInvocationTask afterTask = new GradleInvoker.AfterGradleInvocationTask() {
+      @Override
+      public void execute(@NotNull GradleInvocationResult result) {
+        success.set(result.isBuildSuccessful());
+        gradleInvoker.removeAfterGradleInvocationTask(this);
+        done.up();
+      }
+    };
+
+    ExternalSystemTaskId taskId = ExternalSystemTaskId.create(GRADLE_SYSTEM_ID, EXECUTE_TASK, project);
+    List<String> jvmArguments = ImmutableList.of();
+
+    // https://code.google.com/p/android/issues/detail?id=213040 - make split apks only available if an env var is set
+    List<String> args = new ArrayList<>(commandLineArguments);
+    if (!Boolean.valueOf(System.getenv(GradleTaskRunner.USE_SPLIT_APK))) {
+      // force multi dex when the env var is not set to true
+      args.add(AndroidGradleSettings.createProjectProperty(AndroidProject.PROPERTY_SIGNING_COLDSWAP_MODE, "MULTIDEX"));
+    }
+
+    // To ensure that the "Run Configuration" waits for the Gradle tasks to be executed, we use SwingUtilities.invokeAndWait. I tried
+    // using Application.invokeAndWait but it never worked. IDEA also uses SwingUtilities in this scenario (see CompileStepBeforeRun.)
+    SwingUtilities.invokeAndWait(() -> {
+      gradleInvoker.addAfterGradleInvocationTask(afterTask);
+      gradleInvoker.executeTasks(
+        tasks,
+        jvmArguments,
+        args,
+        taskId,
+        new GradleNotificationListener(),
+        instantRunGradleBuildFile,
+        false,
+        true
+      );
+    });
+
+    done.waitFor();
+    return success.get();
+  }
+
+  class GradleNotificationListener extends ExternalSystemTaskNotificationListenerAdapter {
+    @Override
+    public void onTaskOutput(@NotNull ExternalSystemTaskId id, @NotNull String text, boolean stdOut) {
+      super.onTaskOutput(id, text, stdOut);
+      String toPrint = text.trim();
+      if (!Strings.isNullOrEmpty(toPrint)) {
+        context.output(new PrintOutput(toPrint, stdOut ? PrintOutput.OutputType.NORMAL : PrintOutput.OutputType.ERROR));
+      }
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunTasksProvider.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunTasksProvider.java
new file mode 100644
index 0000000..f528f25
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/BlazeInstantRunTasksProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.instantrun;
+
+import com.android.tools.idea.fd.InstantRunTasksProvider;
+import com.google.common.collect.ImmutableList;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Returns blaze-specific instant run tasks.
+ */
+public class BlazeInstantRunTasksProvider implements InstantRunTasksProvider {
+  @NotNull
+  @Override
+  public List<String> getCleanAndGenerateSourcesTasks() {
+    return ImmutableList.of();
+  }
+
+  @NotNull
+  @Override
+  public List<String> getFullBuildTasks() {
+    return ImmutableList.of("process");
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java
new file mode 100644
index 0000000..ef58a9b
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/instantrun/InstantRunExperiment.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.instantrun;
+
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+
+/**
+ * Holds the instant run experiment
+ */
+public class InstantRunExperiment {
+  public static final BoolExperiment INSTANT_RUN_ENABLED = new BoolExperiment("instant.run.enabled", false);
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java
new file mode 100644
index 0000000..f7603b7
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeAndroidBinaryMobileInstallRunContext.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.mobileinstall;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.ApplicationIdProvider;
+import com.android.tools.idea.run.ConsoleProvider;
+import com.android.tools.idea.run.LaunchOptions;
+import com.android.tools.idea.run.activity.DefaultStartActivityFlagsProvider;
+import com.android.tools.idea.run.activity.StartActivityFlagsProvider;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.tasks.DebugConnectorTask;
+import com.android.tools.idea.run.tasks.LaunchTask;
+import com.android.tools.idea.run.tasks.LaunchTasksProvider;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryApplicationIdProvider;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryApplicationLaunchTaskProvider;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryConsoleProvider;
+import com.google.idea.blaze.android.run.binary.BlazeAndroidBinaryRunConfigurationState;
+import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.google.idea.blaze.android.run.runner.*;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.util.Set;
+
+/**
+ * Run context for android_binary.
+ */
+public class BlazeAndroidBinaryMobileInstallRunContext implements BlazeAndroidRunContext {
+
+  private final Project project;
+  private final AndroidFacet facet;
+  private final RunConfiguration runConfiguration;
+  private final ExecutionEnvironment env;
+  private final BlazeAndroidBinaryRunConfigurationState configState;
+  private final ConsoleProvider consoleProvider;
+  private final ApplicationIdProvider applicationIdProvider;
+  private final BlazeApkBuildStepMobileInstall buildStep;
+
+  public BlazeAndroidBinaryMobileInstallRunContext(Project project,
+                                                   AndroidFacet facet,
+                                                   RunConfiguration runConfiguration,
+                                                   ExecutionEnvironment env,
+                                                   BlazeAndroidRunConfigurationCommonState commonState,
+                                                   BlazeAndroidBinaryRunConfigurationState configState,
+                                                   ImmutableList<String> buildFlags) {
+    this.project = project;
+    this.facet = facet;
+    this.runConfiguration = runConfiguration;
+    this.env = env;
+    this.configState = configState;
+    this.consoleProvider = new BlazeAndroidBinaryConsoleProvider(project);
+    this.buildStep = new BlazeApkBuildStepMobileInstall(project, env, commonState, buildFlags, configState.isUseSplitApksIfPossible());
+    this.applicationIdProvider = new BlazeAndroidBinaryApplicationIdProvider(project, buildStep.getDeployInfo());
+  }
+
+  @Override
+  public BlazeAndroidDeviceSelector getDeviceSelector() {
+    return new BlazeAndroidDeviceSelector.NormalDeviceSelector();
+  }
+
+  @Override
+  public void augmentEnvironment(ExecutionEnvironment env) {
+  }
+
+  @Override
+  public void augmentLaunchOptions(@NotNull LaunchOptions.Builder options) {
+    options
+      .setDeploy(false)
+      .setPmInstallOptions(configState.ACTIVITY_EXTRA_FLAGS)
+      .setOpenLogcatAutomatically(true);
+  }
+
+  @NotNull
+  @Override
+  public ConsoleProvider getConsoleProvider() {
+    return consoleProvider;
+  }
+
+  @Override
+  public ApplicationIdProvider getApplicationIdProvider() throws ExecutionException {
+    return applicationIdProvider;
+  }
+
+  @Override
+  public BlazeApkBuildStep getBuildStep() {
+    return buildStep;
+  }
+
+  @Override
+  public LaunchTasksProvider getLaunchTasksProvider(
+    LaunchOptions launchOptions,
+    BlazeAndroidRunConfigurationDebuggerManager debuggerManager) throws ExecutionException {
+    return new BlazeAndroidLaunchTasksProvider(project, this, applicationIdProvider, launchOptions, debuggerManager);
+  }
+
+  @Override
+  public ImmutableList<LaunchTask> getDeployTasks(IDevice device, LaunchOptions launchOptions) throws ExecutionException {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public LaunchTask getApplicationLaunchTask(LaunchOptions launchOptions,
+                                             AndroidDebugger androidDebugger,
+                                             AndroidDebuggerState androidDebuggerState,
+                                             ProcessHandlerLaunchStatus processHandlerLaunchStatus) throws ExecutionException {
+    final StartActivityFlagsProvider startActivityFlagsProvider = new DefaultStartActivityFlagsProvider(
+      androidDebugger,
+      androidDebuggerState,
+      project,
+      launchOptions.isDebug(),
+      configState.ACTIVITY_EXTRA_FLAGS
+    );
+
+    BlazeAndroidDeployInfo deployInfo = Futures.get(buildStep.getDeployInfo(), ExecutionException.class);
+
+    return BlazeAndroidBinaryApplicationLaunchTaskProvider.getApplicationLaunchTask(
+      project,
+      applicationIdProvider,
+      deployInfo.getMergedManifestFile(),
+      configState,
+      startActivityFlagsProvider,
+      processHandlerLaunchStatus
+    );
+  }
+
+  @Nullable
+  @Override
+  public DebugConnectorTask getDebuggerTask(LaunchOptions launchOptions,
+                                            AndroidDebugger androidDebugger,
+                                            AndroidDebuggerState androidDebuggerState,
+                                            Set<String> packageIds) throws ExecutionException {
+    //noinspection unchecked
+    return androidDebugger.getConnectDebuggerTask(env, null, packageIds, facet, androidDebuggerState, runConfiguration.getType().getId());
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
new file mode 100644
index 0000000..9923856
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/BlazeApkBuildStepMobileInstall.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.mobileinstall;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.DeviceFutures;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.google.idea.blaze.android.run.deployinfo.BlazeApkDeployInfoProtoHelper;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidDeviceSelector;
+import com.google.idea.blaze.android.run.runner.BlazeApkBuildStep;
+import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
+import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.nio.file.Paths;
+import java.util.concurrent.CancellationException;
+
+/**
+ * Builds and installs the APK using mobile-install.
+ */
+public class BlazeApkBuildStepMobileInstall implements BlazeApkBuildStep {
+  private static final BoolExperiment USE_SDK_ADB = new BoolExperiment("use.sdk.adb", true);
+
+  private final Project project;
+  private final ExecutionEnvironment env;
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+  private final ImmutableList<String> buildFlags;
+  private final boolean useSplitApksIfPossible;
+  private final SettableFuture<BlazeAndroidDeployInfo> deployInfoFuture = SettableFuture.create();
+
+  public BlazeApkBuildStepMobileInstall(Project project,
+                                        ExecutionEnvironment env,
+                                        BlazeAndroidRunConfigurationCommonState commonState,
+                                        ImmutableList<String> buildFlags,
+                                        boolean useSplitApksIfPossible) {
+    this.project = project;
+    this.env = env;
+    this.commonState = commonState;
+    this.buildFlags = buildFlags;
+    this.useSplitApksIfPossible = useSplitApksIfPossible;
+  }
+
+  @Override
+  public boolean build(BlazeContext context, BlazeAndroidDeviceSelector.DeviceSession deviceSession) {
+    final ScopedTask buildTask = new ScopedTask(context) {
+      @Override
+      protected void execute(@NotNull BlazeContext context) {
+        boolean incrementalInstall = env.getExecutor() instanceof IncrementalInstallExecutor;
+
+        DeviceFutures deviceFutures = deviceSession.deviceFutures;
+        assert deviceFutures != null;
+        IDevice device = resolveDevice(context, deviceFutures);
+        if (device == null) {
+          return;
+        }
+        BlazeCommand.Builder command = BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.MOBILE_INSTALL);
+        command.addBlazeFlags(BlazeFlags.adbSerialFlags(device.getSerialNumber()));
+
+        if (USE_SDK_ADB.getValue()) {
+          File adb = getSdkAdb(project);
+          if (adb != null) {
+            command.addBlazeFlags(ImmutableList.of("--adb", adb.toString()));
+          }
+        }
+
+        // split-apks only supported for API level 23 and above
+        if (useSplitApksIfPossible && device.getVersion().getApiLevel() >= 23) {
+          command.addBlazeFlags(BlazeFlags.SPLIT_APKS);
+        }
+        else if (incrementalInstall) {
+          command.addBlazeFlags(BlazeFlags.INCREMENTAL);
+        }
+        WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+
+        command
+          .addTargets(commonState.getTarget())
+          .addBlazeFlags(buildFlags)
+          .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS)
+        ;
+
+        BlazeApkDeployInfoProtoHelper deployInfoHelper = new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+
+        SaveUtil.saveAllFiles();
+        int retVal = ExternalTask.builder(workspaceRoot, command.build())
+          .context(context)
+          .stderr(LineProcessingOutputStream.of(
+            deployInfoHelper.getLineProcessor(),
+            new IssueOutputLineProcessor(project, context, workspaceRoot)))
+          .build()
+          .run();
+        LocalFileSystem.getInstance().refresh(true);
+
+        if (retVal != 0) {
+          context.setHasError();
+          return;
+        }
+
+        BlazeAndroidDeployInfo deployInfo = deployInfoHelper.readDeployInfo(context);
+        if (deployInfo == null) {
+          IssueOutput.error("Could not read apk deploy info from build").submit(context);
+          return;
+        }
+        deployInfoFuture.set(deployInfo);
+      }
+    };
+
+    ListenableFuture<Void> buildFuture = BlazeExecutor.submitTask(
+      project,
+      String.format("Executing %s apk build", Blaze.buildSystemName(project)),
+      buildTask
+    );
+
+    try {
+      Futures.get(buildFuture, ExecutionException.class);
+    }
+    catch (ExecutionException e) {
+      context.setHasError();
+    }
+    catch (CancellationException e) {
+      context.setCancelled();
+    }
+    return context.shouldContinue();
+  }
+
+  public ListenableFuture<BlazeAndroidDeployInfo> getDeployInfo() {
+    return deployInfoFuture;
+  }
+
+  private static File getSdkAdb(Project project) {
+    BlazeProjectData projectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return null;
+    }
+    BlazeAndroidSyncData syncData = projectData.syncState.get(BlazeAndroidSyncData.class);
+    if (syncData == null) {
+      return null;
+    }
+    AndroidSdkPlatform androidSdkPlatform = syncData.androidSdkPlatform;
+    if (androidSdkPlatform == null) {
+      return null;
+    }
+    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
+    if (sdk == null) {
+      return null;
+    }
+    String homePath = sdk.getHomePath();
+    if (homePath == null) {
+      return null;
+    }
+    File adb = Paths.get(homePath, "platform-tools", "adb").toFile();
+    if (!adb.exists()) {
+      return null;
+    }
+    return adb;
+  }
+
+  @Nullable
+  private static IDevice resolveDevice(@NotNull BlazeContext context, @NotNull DeviceFutures deviceFutures) {
+    if (deviceFutures.get().size() != 1) {
+      IssueOutput
+        .error("Only one device can be used with mobile-install.")
+        .submit(context);
+      return null;
+    }
+    context.output(new PrintOutput("Waiting for mobile-install device target..."));
+    try {
+      return Futures.get(
+        Iterables.getOnlyElement(deviceFutures.get()),
+        ExecutionException.class
+      );
+    } catch (ExecutionException|UncheckedExecutionException e) {
+      IssueOutput
+        .error("Could not get device: " + e.getMessage())
+        .submit(context);
+      return null;
+    } catch (CancellationException e) {
+      // The user cancelled the device launch.
+      context.setCancelled();
+      return null;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallDebugExecutor.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallDebugExecutor.java
new file mode 100644
index 0000000..c20f13c
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallDebugExecutor.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.run.binary.mobileinstall;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import icons.BlazeAndroidIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+/**
+ * Executor for running blaze mobile-install --incremental and then launching the current run
+ * configuration in debug mode. This executor adds a launch button that is only enabled for
+ * mobile-install run configurations.
+ */
+public class IncrementalInstallDebugExecutor
+  extends DefaultDebugExecutor implements IncrementalInstallExecutor {
+  public static final String EXECUTOR_ID = "blaze.incremental.install.debug";
+
+  @NotNull
+  @Override
+  public Icon getIcon() {
+    return BlazeAndroidIcons.MobileInstallDebug;
+  }
+
+  @NotNull
+  @Override
+  public String getActionName() {
+    return Blaze.guessBuildSystemName() + " incremental install and debug";
+  }
+
+  @Override
+  public String getContextActionId() {
+    return "IncrementalInstallDebugClass";
+  }
+
+  @NotNull
+  @Override
+  public String getStartActionText() {
+    return Blaze.guessBuildSystemName() + " incremental install and debug";
+  }
+
+  @Override
+  public String getDescription() {
+    return Blaze.guessBuildSystemName().toLowerCase() + " mobile-install --incremental, launch with debugger";
+  }
+
+  @NotNull
+  @Override
+  public String getId() {
+    return EXECUTOR_ID;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallExecutor.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallExecutor.java
new file mode 100644
index 0000000..e8bba2e
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallExecutor.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.binary.mobileinstall;
+
+/**
+ * A marker interface for executors that specify --incremental should be used
+ * with mobile-install.
+ */
+public interface IncrementalInstallExecutor {
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallRunExecutor.java b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallRunExecutor.java
new file mode 100644
index 0000000..3e98d87
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/binary/mobileinstall/IncrementalInstallRunExecutor.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.run.binary.mobileinstall;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.executors.DefaultRunExecutor;
+import icons.BlazeAndroidIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+/**
+ * Executor for running blaze mobile-install --incremental and then launching the current run
+ * configuration. This executor adds a launch button that is only enabled for mobile-install run
+ * configurations.
+ */
+public class IncrementalInstallRunExecutor
+  extends DefaultRunExecutor implements IncrementalInstallExecutor {
+  public static final String EXECUTOR_ID = "blaze.incremental.install.run";
+
+  @NotNull
+  @Override
+  public Icon getIcon() {
+    return BlazeAndroidIcons.MobileInstallRun;
+  }
+
+  @NotNull
+  @Override
+  public String getActionName() {
+    return Blaze.guessBuildSystemName() + " incremental install and run";
+  }
+
+  @Override
+  public String getContextActionId() {
+    return "IncrementalInstallRunClass";
+  }
+
+  @NotNull
+  @Override
+  public String getStartActionText() {
+    return Blaze.guessBuildSystemName() + " incremental install and run";
+  }
+
+  @Override
+  public String getDescription() {
+    return Blaze.guessBuildSystemName().toLowerCase() + "mobile-install --incremental, run";
+  }
+
+  @NotNull
+  @Override
+  public String getId() {
+    return EXECUTOR_ID;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java
new file mode 100644
index 0000000..1e3c0bd
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeAndroidDeployInfo.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.deployinfo;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.manifest.ManifestParser;
+import com.google.repackaged.devtools.build.lib.rules.android.deployinfo.AndroidDeployInfoOuterClass;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.dom.manifest.Manifest;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Info about the android_binary/android_test to deploy.
+ */
+public class BlazeAndroidDeployInfo {
+  private final Project project;
+  private final File executionRoot;
+  private final AndroidDeployInfoOuterClass.AndroidDeployInfo deployInfo;
+
+  public BlazeAndroidDeployInfo(Project project,
+                                File executionRoot,
+                                AndroidDeployInfoOuterClass.AndroidDeployInfo deployInfo) {
+    this.project = project;
+    this.executionRoot = executionRoot;
+    this.deployInfo = deployInfo;
+  }
+
+  @Nullable
+  public File getMergedManifestFile() {
+    return new File(executionRoot, deployInfo.getMergedManifest().getExecRootPath());
+  }
+
+  @Nullable
+  public Manifest getMergedManifest() {
+    File manifestFile = getMergedManifestFile();
+    return ManifestParser.getInstance(project).getManifest(manifestFile);
+  }
+
+  public List<File> getAdditionalMergedManifestFiles() {
+    return deployInfo.getAdditionalMergedManifestsList().stream()
+      .map(artifact -> new File(executionRoot, artifact.getExecRootPath()))
+      .collect(Collectors.toList());
+  }
+
+  public List<Manifest> getAdditionalMergedManifests() {
+    return getAdditionalMergedManifestFiles().stream()
+      .map(file -> ManifestParser.getInstance(project).getManifest(file))
+      .filter(Objects::nonNull)
+      .collect(Collectors.toList());
+  }
+
+  public List<File> getManifestFiles() {
+    List<File> result = Lists.newArrayList();
+    result.add(getMergedManifestFile());
+    result.addAll(getAdditionalMergedManifestFiles());
+    return result;
+  }
+
+  /**
+   * Returns the full list of apks to deploy, if any.
+   */
+  public List<File> getApksToDeploy() {
+    return deployInfo.getApksToDeployList().stream()
+      .map(artifact -> new File(executionRoot, artifact.getExecRootPath()))
+      .collect(Collectors.toList());
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
new file mode 100644
index 0000000..9628565
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkDeployInfoProtoHelper.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.deployinfo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.android.manifest.ManifestParser;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.repackaged.devtools.build.lib.rules.android.deployinfo.AndroidDeployInfoOuterClass;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Reads the deploy info from a build step.
+ */
+public class BlazeApkDeployInfoProtoHelper {
+  private static final Logger LOG = Logger.getInstance(BlazeApkDeployInfoProtoHelper.class);
+
+  private final Project project;
+  private final WorkspaceRoot workspaceRoot;
+  private final ImmutableList<String> buildFlags;
+  private final List<File> deployInfoFiles = Lists.newArrayList();
+  private final LineProcessingOutputStream.LineProcessor lineProcessor =
+    new ExperimentalShowArtifactsLineProcessor(deployInfoFiles, ".deployinfo.pb");
+
+  public BlazeApkDeployInfoProtoHelper(Project project, ImmutableList<String> buildFlags) {
+    this.project = project;
+    this.buildFlags = buildFlags;
+    this.workspaceRoot = WorkspaceRoot.fromProject(project);
+  }
+
+  public LineProcessingOutputStream.LineProcessor getLineProcessor() {
+    return lineProcessor;
+  }
+
+  @Nullable
+  public BlazeAndroidDeployInfo readDeployInfo(BlazeContext context) {
+    File deployInfoFile = Iterables.getOnlyElement(deployInfoFiles, null);
+    if (deployInfoFile == null) {
+      return null;
+    }
+    AndroidDeployInfoOuterClass.AndroidDeployInfo deployInfo;
+    try (InputStream inputStream = new FileInputStream(deployInfoFile)) {
+      deployInfo = AndroidDeployInfoOuterClass.AndroidDeployInfo.parseFrom(inputStream);
+    } catch (IOException e) {
+      LOG.error(e);
+      return null;
+    }
+    String executionRoot = getExecutionRoot(context);
+    if (executionRoot == null) {
+      return null;
+    }
+    BlazeAndroidDeployInfo androidDeployInfo = new BlazeAndroidDeployInfo(project, new File(executionRoot), deployInfo);
+
+    List<File> manifestFiles = androidDeployInfo.getManifestFiles();
+    ManifestParser.getInstance(project).refreshManifests(manifestFiles);
+
+    return androidDeployInfo;
+  }
+
+  @Nullable
+  private String getExecutionRoot(BlazeContext context) {
+    ListenableFuture<String> execRootFuture = BlazeInfo.getInstance().runBlazeInfo(
+      context, Blaze.getBuildSystem(project),
+      workspaceRoot,
+      buildFlags,
+      BlazeInfo.EXECUTION_ROOT_KEY
+    );
+    try {
+      return execRootFuture.get();
+    }
+    catch (InterruptedException e) {
+      context.setCancelled();
+    }
+    catch (ExecutionException e) {
+      LOG.error(e);
+      context.setHasError();
+    }
+    return null;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkProvider.java b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkProvider.java
new file mode 100644
index 0000000..5bf27c0
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/deployinfo/BlazeApkProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.deployinfo;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.ApkInfo;
+import com.android.tools.idea.run.ApkProvider;
+import com.android.tools.idea.run.ApkProvisionException;
+import com.android.tools.idea.run.ValidationError;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.android.run.runner.AaptUtil;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Apk provider from deploy info proto
+ */
+public class BlazeApkProvider implements ApkProvider {
+  private final Project project;
+  private final ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture;
+
+  public BlazeApkProvider(Project project,
+                          ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture) {
+    this.project = project;
+    this.deployInfoFuture = deployInfoFuture;
+  }
+
+  @NotNull
+  @Override
+  public Collection<ApkInfo> getApks(@NotNull IDevice device) throws ApkProvisionException {
+    BlazeAndroidDeployInfo deployInfo = Futures.get(deployInfoFuture, ApkProvisionException.class);
+    ImmutableList.Builder<ApkInfo> apkInfos = ImmutableList.builder();
+    for (File apk : deployInfo.getApksToDeploy())  {
+      apkInfos.add(new ApkInfo(apk, manifestPackageForApk(apk)));
+    }
+    return apkInfos.build();
+  }
+
+  @NotNull
+  private String manifestPackageForApk(@NotNull final File apk) throws ApkProvisionException {
+    try {
+      return AaptUtil.getApkManifestPackage(project, apk);
+    }
+    catch (AaptUtil.AaptUtilException e) {
+      throw new ApkProvisionException(
+        "Could not determine manifest package for apk: " + apk.getPath()
+        + "\nbecause: " + e.getMessage(),
+        e);
+    }
+  }
+
+  @NotNull
+  @Override
+  public List<ValidationError> validate() {
+    return ImmutableList.of();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/AaptUtil.java b/aswb/src/com/google/idea/blaze/android/run/runner/AaptUtil.java
new file mode 100644
index 0000000..9fc61a7
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/AaptUtil.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.runner;
+
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.BuildToolInfo.PathId;
+import com.google.idea.blaze.android.sdk.SdkUtil;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.OSProcessHandler;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.sdk.AndroidPlatform;
+
+import javax.annotation.Nullable;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A collection of utilities for extracting information from APKs using aapt.
+ */
+public final class AaptUtil {
+
+  private static final Pattern DEBUGGABLE_PATTERN = Pattern.compile("^application-debuggable$");
+  private static final Pattern PACKAGE_PATTERN = Pattern.compile("^package: .*name='([\\w\\.]+)'");
+  private static final Pattern LAUNCHABLE_PATTERN =
+    Pattern.compile("^launchable-activity: .*name='([\\w\\.]+)'");
+
+  public static class AaptUtilException extends Exception {
+    public AaptUtilException(String message) {
+      super(message);
+    }
+
+    public AaptUtilException(String message, Throwable cause) {
+      super(message, cause);
+    }
+  }
+
+  private AaptUtil() {
+  }
+
+  /**
+   * Determines whether the given APK is debuggable. Trying to debug a non-debuggable APK on a
+   * release-keys device will fail.
+   */
+  public static boolean isApkDebuggable(
+    Project project,
+    File apk
+  ) throws AaptUtilException {
+    return getAaptBadging(project, apk, DEBUGGABLE_PATTERN) != null;
+  }
+
+  /**
+   * Determines the manifest package name for the given APK.
+   */
+  public static String getApkManifestPackage(
+    Project project,
+    File apk
+  ) throws AaptUtilException {
+    MatchResult packageResult = getAaptBadging(project, apk, PACKAGE_PATTERN);
+    if (packageResult == null) {
+      throw new AaptUtilException(
+        "No match found in `aapt dump badging` for package manifest pattern.");
+    }
+    return packageResult.group(1);
+  }
+
+  /**
+   * Determines the default launchable activity for the given apk.
+   */
+  public static String getLaunchableActivity(
+    Project project,
+    File apk
+  ) throws AaptUtilException {
+    MatchResult activityResult = getAaptBadging(project, apk, LAUNCHABLE_PATTERN);
+    if (activityResult == null) {
+      throw new AaptUtilException(
+        "No match found in `aapt dump badging` for launchable activity pattern.");
+    }
+    return activityResult.group(1);
+  }
+
+  /**
+   * Uses aapt to dump badging information for the given apk, and extracts information from the
+   * output matching the given pattern.
+   */
+  @Nullable
+  private static MatchResult getAaptBadging(
+    Project project,
+    File apk,
+    Pattern pattern
+  ) throws AaptUtilException {
+    if (!apk.exists()) {
+      throw new AaptUtilException("apk file does not exist: " + apk);
+    }
+    AndroidPlatform androidPlatform = SdkUtil.getAndroidPlatform(project);
+    if (androidPlatform == null) {
+      throw new AaptUtilException(
+        "Could not find Android platform sdk for project " + project.getName());
+    }
+    BuildToolInfo toolInfo = androidPlatform.getSdkData().getLatestBuildTool();
+    if (toolInfo == null) {
+      throw new AaptUtilException(
+        "Could not find Android sdk build-tools for project " + project.getName());
+    }
+    String aapt = toolInfo.getPath(PathId.AAPT);
+    GeneralCommandLine commandLine = new GeneralCommandLine(
+      aapt,
+      "dump",
+      "badging",
+      apk.getAbsolutePath());
+    OSProcessHandler handler;
+    try {
+      handler = new OSProcessHandler(commandLine);
+    }
+    catch (ExecutionException e) {
+      throw new AaptUtilException("Could not execute aapt to extract apk information.", e);
+    }
+
+    // The wrapped stream is closed by the process handler.
+    BufferedReader reader = new BufferedReader(
+      new InputStreamReader(handler.getProcess().getInputStream()));
+    try {
+      String line;
+      while ((line = reader.readLine()) != null) {
+        Matcher matcher = pattern.matcher(line);
+        if (matcher.find()) {
+          return matcher.toMatchResult();
+        }
+      }
+    }
+    catch (IOException e) {
+      throw new AaptUtilException("Could not read aapt output.", e);
+    }
+    return null;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidDeviceSelector.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidDeviceSelector.java
new file mode 100644
index 0000000..bc6737a
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidDeviceSelector.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.runner;
+
+import com.android.tools.idea.run.AndroidSessionInfo;
+import com.android.tools.idea.run.DeviceFutures;
+import com.android.tools.idea.run.editor.DeployTarget;
+import com.android.tools.idea.run.editor.DeployTargetState;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.Messages;
+import org.jetbrains.android.facet.AndroidFacet;
+
+import javax.annotation.Nullable;
+
+import static org.jetbrains.android.actions.RunAndroidAvdManagerAction.getName;
+
+/**
+ * Selects a device.
+ */
+public interface BlazeAndroidDeviceSelector {
+
+  class DeviceSession {
+    @Nullable public final DeployTarget deployTarget;
+    @Nullable public final DeviceFutures deviceFutures;
+    @Nullable public final AndroidSessionInfo sessionInfo;
+
+    public DeviceSession(@Nullable DeployTarget deployTarget,
+                         @Nullable DeviceFutures deviceFutures,
+                         @Nullable AndroidSessionInfo sessionInfo) {
+      this.deployTarget = deployTarget;
+      this.deviceFutures = deviceFutures;
+      this.sessionInfo = sessionInfo;
+    }
+  }
+
+  DeviceSession getDevice(Project project,
+                          AndroidFacet facet,
+                          BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager,
+                          Executor executor,
+                          ExecutionEnvironment env,
+                          AndroidSessionInfo info,
+                          boolean debug,
+                          int runConfigId) throws ExecutionException;
+
+  class NormalDeviceSelector implements BlazeAndroidDeviceSelector {
+
+    private static final DialogWrapper.DoNotAskOption ourKillLaunchOption = new KillLaunchDialogOption();
+    private static final Logger LOG = Logger.getInstance(NormalDeviceSelector.class);
+
+    static class KillLaunchDialogOption implements DialogWrapper.DoNotAskOption {
+      private boolean show;
+
+      @Override
+      public boolean isToBeShown() {
+        return !show;
+      }
+
+      @Override
+      public void setToBeShown(boolean toBeShown, int exitCode) {
+        show = !toBeShown;
+      }
+
+      @Override
+      public boolean canBeHidden() {
+        return true;
+      }
+
+      @Override
+      public boolean shouldSaveOptionsOnCancel() {
+        return true;
+      }
+
+      @Override
+      public String getDoNotShowMessage() {
+        return "Do not ask again";
+      }
+    }
+
+    @Override
+    public DeviceSession getDevice(Project project,
+                                   AndroidFacet facet,
+                                   BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager,
+                                   Executor executor,
+                                   ExecutionEnvironment env,
+                                   AndroidSessionInfo info,
+                                   boolean debug,
+                                   int runConfigId) throws ExecutionException {
+      // If there is an existing session, then terminate those sessions
+      if (info != null) {
+        boolean continueLaunch = promptAndKillSession(executor, project, info);
+        if (!continueLaunch) {
+          return null;
+        }
+      }
+
+      DeployTarget deployTarget = deployTargetManager.getDeployTarget(executor, env, facet);
+      if (deployTarget == null) {
+        return null;
+      }
+
+      DeviceFutures deviceFutures = null;
+      DeployTargetState deployTargetState = deployTargetManager.getCurrentDeployTargetState();
+      if (!deployTarget.hasCustomRunProfileState(executor)) {
+        deviceFutures = deployTarget.getDevices(
+          deployTargetState,
+          facet,
+          deployTargetManager.getDeviceCount(),
+          debug,
+          runConfigId
+        );
+      }
+      return new DeviceSession(deployTarget, deviceFutures, info);
+    }
+
+    private boolean promptAndKillSession(Executor executor, Project project, AndroidSessionInfo info) {
+      String previousExecutor = info.getExecutorId();
+      String currentExecutor = executor.getId();
+
+      if (ourKillLaunchOption.isToBeShown()) {
+        String msg, noText;
+        if (previousExecutor.equals(currentExecutor)) {
+          msg = String.format("Restart App?\nThe app is already running. Would you like to kill it and restart the session?");
+          noText = "Cancel";
+        }
+        else {
+          msg = String.format("To switch from %1$s to %2$s, the app has to restart. Continue?", previousExecutor, currentExecutor);
+          noText = "Cancel " + currentExecutor;
+        }
+
+        String title = "Launching " + getName();
+        String yesText = "Restart " + getName();
+        if (Messages.NO ==
+            Messages.showYesNoDialog(project, msg, title, yesText, noText, AllIcons.General.QuestionDialog, ourKillLaunchOption)) {
+          return false;
+        }
+      }
+
+      LOG.info("Disconnecting existing session of the same launch configuration");
+      info.getProcessHandler().detachProcess();
+      return true;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java
new file mode 100644
index 0000000..79b21cd
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidLaunchTasksProvider.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.runner;
+
+import com.android.ddmlib.IDevice;
+import com.android.sdklib.AndroidVersion;
+import com.android.tools.idea.run.*;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.tasks.*;
+import com.android.tools.idea.run.util.LaunchStatus;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.intellij.execution.ExecutionException;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Normal launch tasks provider.
+ */
+public class BlazeAndroidLaunchTasksProvider implements LaunchTasksProvider {
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidLaunchTasksProvider.class);
+
+  private final Project project;
+  private final BlazeAndroidRunContext runContext;
+  private final ApplicationIdProvider applicationIdProvider;
+  private final LaunchOptions launchOptions;
+  private final BlazeAndroidRunConfigurationDebuggerManager debuggerManager;
+
+  public BlazeAndroidLaunchTasksProvider(Project project,
+                                         BlazeAndroidRunContext runContext,
+                                         ApplicationIdProvider applicationIdProvider,
+                                         LaunchOptions launchOptions,
+                                         BlazeAndroidRunConfigurationDebuggerManager debuggerManager) {
+    this.project = project;
+    this.runContext = runContext;
+    this.applicationIdProvider = applicationIdProvider;
+    this.launchOptions = launchOptions;
+    this.debuggerManager = debuggerManager;
+  }
+
+  @NotNull
+  @Override
+  public List<LaunchTask> getTasks(@NotNull IDevice device,
+                                   @NotNull LaunchStatus launchStatus,
+                                   @NotNull ConsolePrinter consolePrinter) {
+    final List<LaunchTask> launchTasks = Lists.newArrayList();
+
+    if (launchOptions.isClearLogcatBeforeStart()) {
+      launchTasks.add(new ClearLogcatTask(project));
+    }
+
+    launchTasks.add(new DismissKeyguardTask());
+
+    if (launchOptions.isDeploy()) {
+      try {
+        ImmutableList<LaunchTask> deployTasks = runContext.getDeployTasks(device, launchOptions);
+        launchTasks.addAll(deployTasks);
+      }
+      catch (ExecutionException e) {
+        launchStatus.terminateLaunch(e.getMessage());
+        return ImmutableList.of();
+      }
+    }
+    if (launchStatus.isLaunchTerminated()) {
+      return launchTasks;
+    }
+
+    String packageName;
+    try {
+      packageName = applicationIdProvider.getPackageName();
+
+      ProcessHandlerLaunchStatus processHandlerLaunchStatus = (ProcessHandlerLaunchStatus)launchStatus;
+      LaunchTask appLaunchTask = runContext.getApplicationLaunchTask(
+        launchOptions,
+        debuggerManager.getAndroidDebugger(),
+        debuggerManager.getAndroidDebuggerState(),
+        processHandlerLaunchStatus
+      );
+      if (appLaunchTask != null) {
+        launchTasks.add(appLaunchTask);
+      }
+    }
+    catch (ApkProvisionException e) {
+      LOG.error(e);
+      launchStatus.terminateLaunch("Unable to determine application id: " + e);
+      return ImmutableList.of();
+    }
+    catch (ExecutionException e) {
+      launchStatus.terminateLaunch(e.getMessage());
+      return ImmutableList.of();
+    }
+
+    if (!launchOptions.isDebug() && launchOptions.isOpenLogcatAutomatically()) {
+      launchTasks.add(new ShowLogcatTask(project, packageName));
+    }
+
+    return launchTasks;
+  }
+
+  @Nullable
+  @Override
+  public DebugConnectorTask getConnectDebuggerTask(@NotNull LaunchStatus launchStatus,
+                                                   @Nullable AndroidVersion version) {
+    if (!launchOptions.isDebug()) {
+      return null;
+    }
+    Set<String> packageIds = Sets.newHashSet();
+    try {
+      String packageName = applicationIdProvider.getPackageName();
+      packageIds.add(packageName);
+    }
+    catch (ApkProvisionException e) {
+      Logger.getInstance(AndroidLaunchTasksProvider.class).error(e);
+    }
+
+    try {
+      String packageName = applicationIdProvider.getTestPackageName();
+      if (packageName != null) {
+        packageIds.add(packageName);
+      }
+    }
+    catch (ApkProvisionException e) {
+      // not as severe as failing to obtain package id for main application
+      Logger.getInstance(AndroidLaunchTasksProvider.class)
+        .warn("Unable to obtain test package name, will not connect debugger if tests don't instantiate main application");
+    }
+
+    AndroidDebugger androidDebugger = debuggerManager.getAndroidDebugger();
+    AndroidDebuggerState androidDebuggerState = debuggerManager.getAndroidDebuggerState();
+
+    if (androidDebugger == null || androidDebuggerState == null) {
+      return null;
+    }
+
+    try {
+      return runContext.getDebuggerTask(
+        launchOptions,
+        androidDebugger,
+        androidDebuggerState,
+        packageIds
+      );
+    }
+    catch (ExecutionException e) {
+      launchStatus.terminateLaunch(e.getMessage());
+      return null;
+    }
+  }
+
+  @Override
+  public boolean createsNewProcess() {
+    return true;
+  }
+
+  @Override
+  public boolean monitorRemoteProcess() {
+    return true;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDebuggerManager.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDebuggerManager.java
new file mode 100644
index 0000000..3d174c9
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDebuggerManager.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.runner;
+
+import com.android.tools.idea.run.ValidationError;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.editor.AndroidJavaDebugger;
+import com.android.tools.ndk.run.editor.NativeAndroidDebuggerState;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.android.cppapi.BlazeNativeDebuggerIdProvider;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.JDOMExternalizable;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+import org.jetbrains.android.facet.AndroidFacet;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages android debugger state for the run configurations.
+ */
+public class BlazeAndroidRunConfigurationDebuggerManager implements JDOMExternalizable {
+  private final Project project;
+  private final Map<String, AndroidDebuggerState> androidDebuggerStates = Maps.newHashMap();
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+
+  BlazeAndroidRunConfigurationDebuggerManager(Project project,
+                                              BlazeAndroidRunConfigurationCommonState commonState) {
+    this.project = project;
+    this.commonState = commonState;
+    for (AndroidDebugger androidDebugger: getAndroidDebuggers()) {
+      this.androidDebuggerStates.put(androidDebugger.getId(), androidDebugger.createState());
+    }
+  }
+
+  List<ValidationError> validate(AndroidFacet facet) {
+    // All of the AndroidDebuggerState classes implement a validate that either does nothing or is specific to gradle so there is no point
+    // in calling validate on our AndroidDebuggerState.
+    return ImmutableList.of();
+  }
+
+  @Nullable
+  AndroidDebugger getAndroidDebugger() {
+    String debuggerID = getDebuggerID();
+    for (AndroidDebugger androidDebugger: getAndroidDebuggers()) {
+      if (androidDebugger.getId().equals(debuggerID)) {
+        return androidDebugger;
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  final <T extends AndroidDebuggerState> T getAndroidDebuggerState() {
+    T androidDebuggerState = getAndroidDebuggerState(getDebuggerID());
+    // Set our working directory to our workspace root for native debugging.
+    if (androidDebuggerState instanceof NativeAndroidDebuggerState) {
+      NativeAndroidDebuggerState nativeState = (NativeAndroidDebuggerState)androidDebuggerState;
+      String workingDirPath = WorkspaceRoot.fromProject(project).directory().getPath();
+      nativeState.setWorkingDir(workingDirPath);
+    }
+    return androidDebuggerState;
+  }
+
+  private static List<AndroidDebugger> getAndroidDebuggers() {
+    // This includes the native debugger(s).
+    return Arrays.asList(AndroidDebugger.EP_NAME.getExtensions());
+  }
+
+  private String getDebuggerID() {
+    BlazeNativeDebuggerIdProvider blazeNativeDebuggerIdProvider = BlazeNativeDebuggerIdProvider.getInstance();
+    return (blazeNativeDebuggerIdProvider != null && commonState.isNativeDebuggingEnabled())
+           ? blazeNativeDebuggerIdProvider.getDebuggerId()
+           : AndroidJavaDebugger.ID;
+  }
+
+  @Nullable
+  private final <T extends AndroidDebuggerState> T getAndroidDebuggerState(String androidDebuggerId) {
+    AndroidDebuggerState state = androidDebuggerStates.get(androidDebuggerId);
+    return (state != null) ? (T)state : null;
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    for (Map.Entry<String, AndroidDebuggerState> entry: androidDebuggerStates.entrySet()) {
+      Element optionElement = element.getChild(entry.getKey());
+      if (optionElement != null) {
+        entry.getValue().readExternal(optionElement);
+      }
+    }
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    for (Map.Entry<String, AndroidDebuggerState> entry: androidDebuggerStates.entrySet()) {
+      Element optionElement = new Element(entry.getKey());
+      element.addContent(optionElement);
+      entry.getValue().writeExternal(optionElement);
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDeployTargetManager.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDeployTargetManager.java
new file mode 100644
index 0000000..3b76283
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationDeployTargetManager.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.runner;
+
+import com.android.tools.idea.run.DeviceCount;
+import com.android.tools.idea.run.LaunchCompatibility;
+import com.android.tools.idea.run.TargetSelectionMode;
+import com.android.tools.idea.run.ValidationError;
+import com.android.tools.idea.run.editor.DeployTarget;
+import com.android.tools.idea.run.editor.DeployTargetProvider;
+import com.android.tools.idea.run.editor.DeployTargetState;
+import com.google.common.collect.ImmutableMap;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.util.DefaultJDOMExternalizer;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.JDOMExternalizable;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+import org.jetbrains.android.facet.AndroidFacet;
+
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Manages deploy target state for run configurations.
+ */
+public class BlazeAndroidRunConfigurationDeployTargetManager implements JDOMExternalizable {
+  private static final String TARGET_SELECTION_MODE = TargetSelectionMode.SHOW_DIALOG.name();
+
+  private final int runConfigId;
+  private final boolean isAndroidTest;
+  private final List<DeployTargetProvider> deployTargetProviders;
+  private final Map<String, DeployTargetState> deployTargetStates;
+
+  BlazeAndroidRunConfigurationDeployTargetManager(int runConfigId,
+                                                  boolean isAndroidTest) {
+    this.runConfigId = runConfigId;
+    this.isAndroidTest = isAndroidTest;
+    this.deployTargetProviders = DeployTargetProvider.getProviders();
+
+    ImmutableMap.Builder<String, DeployTargetState> builder = ImmutableMap.builder();
+    for (DeployTargetProvider provider : deployTargetProviders) {
+      builder.put(provider.getId(), provider.createState());
+    }
+    this.deployTargetStates = builder.build();
+  }
+
+  List<ValidationError> validate(AndroidFacet facet) {
+    return getCurrentDeployTargetState().validate(facet);
+  }
+
+  @Nullable
+  DeployTarget getDeployTarget(Executor executor,
+                               ExecutionEnvironment env,
+                               AndroidFacet facet) throws ExecutionException {
+    DeployTargetProvider currentTargetProvider = getCurrentDeployTargetProvider();
+
+    DeployTarget deployTarget;
+    if (currentTargetProvider.requiresRuntimePrompt()) {
+      deployTarget = currentTargetProvider.showPrompt(
+        executor,
+        env,
+        facet,
+        getDeviceCount(),
+        isAndroidTest,
+        deployTargetStates,
+        runConfigId,
+        (device) -> LaunchCompatibility.YES
+      );
+      if (deployTarget == null) {
+        return null;
+      }
+    }
+    else {
+      deployTarget = currentTargetProvider.getDeployTarget();
+    }
+
+    return deployTarget;
+  }
+
+  DeployTargetState getCurrentDeployTargetState() {
+    DeployTargetProvider currentTarget = getCurrentDeployTargetProvider();
+    return deployTargetStates.get(currentTarget.getId());
+  }
+
+  // TODO(salguarnieri) Currently the target selection mode is always SHOW_DIALOG. This code is here for future use.
+  // If this code still isn't used after ASwB supports native, then we should delete this logic.
+  private DeployTargetProvider getCurrentDeployTargetProvider() {
+    DeployTargetProvider target = getDeployTargetProvider(TARGET_SELECTION_MODE);
+    if (target == null) {
+      target = getDeployTargetProvider(TargetSelectionMode.SHOW_DIALOG.name());
+    }
+
+    assert target != null;
+    return target;
+  }
+
+  @Nullable
+  private DeployTargetProvider getDeployTargetProvider(String id) {
+    for (DeployTargetProvider target : deployTargetProviders) {
+      if (target.getId().equals(id)) {
+        return target;
+      }
+    }
+    return null;
+  }
+
+  DeviceCount getDeviceCount() {
+    return DeviceCount.SINGLE;
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    for (DeployTargetState state : deployTargetStates.values()) {
+      DefaultJDOMExternalizer.readExternal(state, element);
+    }
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    for (DeployTargetState state : deployTargetStates.values()) {
+      DefaultJDOMExternalizer.writeExternal(state, element);
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
new file mode 100644
index 0000000..eed693e
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunConfigurationRunner.java
@@ -0,0 +1,393 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.idea.blaze.android.run.runner;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.*;
+import com.android.tools.idea.run.editor.DeployTarget;
+import com.android.tools.idea.run.editor.DeployTargetState;
+import com.android.tools.idea.run.tasks.LaunchTasksProvider;
+import com.android.tools.idea.run.util.LaunchUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfiguration;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.experiments.ExperimentScope;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.intellij.execution.DefaultExecutionResult;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.ExecutionResult;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.RunProfileState;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.ProgramRunner;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.JDOMExternalizable;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+import org.jetbrains.android.util.AndroidBundle;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+import static com.android.tools.idea.gradle.util.Projects.requiredAndroidModelMissing;
+import static org.jetbrains.android.actions.RunAndroidAvdManagerAction.getName;
+
+/**
+ * Supports the entire run configuration flow. Used by both android_binary and android_test.
+ *
+ * Does any verification necessary, builds the APK and installs it, launches and debug tasks, etc.
+ *
+ * Any indirection between android_binary/android_test, mobile-install, InstantRun etc. should
+ * come via the strategy class.
+ */
+public final class BlazeAndroidRunConfigurationRunner implements JDOMExternalizable {
+
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidRunConfigurationRunner.class);
+
+  private static final String SYNC_FAILED_ERR_MSG = "Project state is invalid. Please sync and try your action again.";
+
+  public static final Key<BlazeAndroidRunContext> RUN_CONTEXT_KEY = Key.create("blaze.run.context");
+  public static final Key<BlazeAndroidDeviceSelector.DeviceSession> DEVICE_SESSION_KEY = Key.create("blaze.device.session");
+
+  // We need to split "-c dbg" into two flags because we pass flags as a list of strings to the command line executor and we need blaze
+  // to see -c and dbg as two separate entities, not one.
+  private static final ImmutableList<String> NATIVE_DEBUG_FLAGS = ImmutableList.of("--fission=no", "-c", "dbg");
+
+  private final Project project;
+
+  private final BlazeAndroidRunConfiguration runConfiguration;
+
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+
+  private final int runConfigId;
+
+  private final BlazeAndroidRunConfigurationDeployTargetManager deployTargetManager;
+
+  private final BlazeAndroidRunConfigurationDebuggerManager debuggerManager;
+
+  public BlazeAndroidRunConfigurationRunner(Project project,
+                                            BlazeAndroidRunConfiguration runConfiguration,
+                                            BlazeAndroidRunConfigurationCommonState commonState,
+                                            boolean isAndroidTest,
+                                            int runConfigId) {
+    this.project = project;
+    this.runConfiguration = runConfiguration;
+    this.commonState = commonState;
+    this.runConfigId = runConfigId;
+    this.deployTargetManager = new BlazeAndroidRunConfigurationDeployTargetManager(runConfigId, isAndroidTest);
+    this.debuggerManager = new BlazeAndroidRunConfigurationDebuggerManager(project, commonState);
+  }
+
+  private ImmutableList<String> getBuildFlags(Project project, ProjectViewSet projectViewSet) {
+    return ImmutableList.<String>builder()
+      .addAll(BlazeFlags.buildFlags(project, projectViewSet))
+      .addAll(commonState.getUserFlags())
+      .addAll(getNativeDebuggerFlags())
+      .build();
+  }
+
+  public ImmutableList<String> getNativeDebuggerFlags() {
+    return commonState.isNativeDebuggingEnabled() ? NATIVE_DEBUG_FLAGS : ImmutableList.of();
+  }
+
+  /**
+   * We collect errors rather than throwing to avoid missing fatal errors by exiting early for a warning.
+   * We use a separate method for the collection so the compiler prevents us from accidentally throwing.
+   */
+  public List<ValidationError> validate(@Nullable Module module) {
+    List<ValidationError> errors = Lists.newArrayList();
+    if (module == null) {
+      errors.add(ValidationError.fatal("No run configuration module found"));
+      return errors;
+    }
+
+    if (commonState.getTarget() == null) {
+      errors.add(ValidationError.fatal("No target selected"));
+      return errors;
+    }
+
+    final Project project = module.getProject();
+    if (requiredAndroidModelMissing(project)) {
+      errors.add(ValidationError.fatal(SYNC_FAILED_ERR_MSG));
+    }
+
+    AndroidFacet facet = AndroidFacet.getInstance(module);
+    if (facet == null) {
+      // Can't proceed.
+      return ImmutableList.of(ValidationError.fatal(AndroidBundle.message("no.facet.error", module.getName())));
+    }
+
+    if (facet.getConfiguration().getAndroidPlatform() == null) {
+      errors.add(ValidationError.fatal(AndroidBundle.message("select.platform.error")));
+    }
+
+    errors.addAll(deployTargetManager.validate(facet));
+    errors.addAll(debuggerManager.validate(facet));
+
+    return errors;
+  }
+
+  @Nullable
+  public final RunProfileState getState(Module module,
+                                        final Executor executor,
+                                        ExecutionEnvironment env) throws ExecutionException {
+
+    assert module != null : "Enforced by fatal validation check in checkConfiguration.";
+    final AndroidFacet facet = AndroidFacet.getInstance(module);
+    assert facet != null : "Enforced by fatal validation check in checkConfiguration.";
+    Project project = env.getProject();
+
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet == null) {
+      throw new ExecutionException("Could not load project view. Please resync project");
+    }
+    ImmutableList<String> buildFlags = getBuildFlags(project, projectViewSet);
+
+    BlazeAndroidRunContext runContext = runConfiguration.createRunContext(
+      project,
+      facet,
+      env,
+      buildFlags
+    );
+
+    runContext.augmentEnvironment(env);
+
+    boolean debug = executor instanceof  DefaultDebugExecutor;
+    if (debug && !AndroidSdkUtils.activateDdmsIfNecessary(facet.getModule().getProject())) {
+      throw new ExecutionException("Unable to obtain debug bridge. Please check if there is a different tool using adb that is active.");
+    }
+
+    AndroidSessionInfo info = AndroidSessionInfo.findOldSession(project, null, runConfigId);
+
+    BlazeAndroidDeviceSelector deviceSelector = runContext.getDeviceSelector();
+    BlazeAndroidDeviceSelector.DeviceSession deviceSession = deviceSelector.getDevice(
+      project,
+      facet,
+      deployTargetManager,
+      executor,
+      env,
+      info,
+      debug,
+      runConfigId
+    );
+    if (deviceSession == null) {
+      return null;
+    }
+
+    DeployTarget deployTarget = deviceSession.deployTarget;
+    if (deployTarget != null && deployTarget.hasCustomRunProfileState(executor)) {
+      DeployTargetState deployTargetState = deployTargetManager.getCurrentDeployTargetState();
+      return deployTarget.getRunProfileState(executor, env, deployTargetState);
+    }
+
+    DeviceFutures deviceFutures = deviceSession.deviceFutures;
+    if (deviceFutures == null) {
+      // The user deliberately canceled, or some error was encountered and exposed by the chooser. Quietly exit.
+      return null;
+    }
+
+    if (deviceFutures.get().isEmpty()) {
+      throw new ExecutionException(AndroidBundle.message("deployment.target.not.found"));
+    }
+
+    if (debug) {
+      String error = canDebug(deviceFutures, facet, module.getName());
+      if (error != null) {
+        throw new ExecutionException(error);
+      }
+    }
+
+    LaunchOptions.Builder launchOptionsBuilder = getDefaultLaunchOptions()
+      .setDebug(debug);
+    runContext.augmentLaunchOptions(launchOptionsBuilder);
+    LaunchOptions launchOptions = launchOptionsBuilder.build();
+
+    // Store the run context on the execution environment so before-run tasks can access it.
+    env.putCopyableUserData(RUN_CONTEXT_KEY, runContext);
+    env.putCopyableUserData(DEVICE_SESSION_KEY, deviceSession);
+
+    return new BlazeAndroidRunState(
+      module,
+      env,
+      getName(),
+      launchOptions,
+      deviceSession,
+      runContext
+    );
+  }
+
+  private static String canDebug(DeviceFutures deviceFutures, AndroidFacet facet, String moduleName) {
+    // If we are debugging on a device, then the app needs to be debuggable
+    for (ListenableFuture<IDevice> future : deviceFutures.get()) {
+      if (!future.isDone()) {
+        // this is an emulator, and we assume that all emulators are debuggable
+        continue;
+      }
+      IDevice device = Futures.getUnchecked(future);
+      if (!LaunchUtils.canDebugAppOnDevice(facet, device)) {
+        return AndroidBundle.message("android.cannot.debug.noDebugPermissions", moduleName, device.getName());
+      }
+    }
+    return null;
+  }
+
+
+  private static LaunchOptions.Builder getDefaultLaunchOptions() {
+    return LaunchOptions.builder()
+      .setClearLogcatBeforeStart(false)
+      .setSkipNoopApkInstallations(true)
+      .setForceStopRunningApp(true);
+  }
+
+  public boolean executeBuild(ExecutionEnvironment env) {
+    boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
+    return Scope.root(context -> {
+      context
+        .push(new IssuesScope(project))
+        .push(new ExperimentScope())
+        .push(new BlazeConsoleScope.Builder(project).setSuppressConsole(suppressConsole).build())
+        .push(new LoggedTimingScope(project, Action.APK_BUILD_AND_INSTALL))
+      ;
+
+      BlazeAndroidRunContext runContext = env.getCopyableUserData(RUN_CONTEXT_KEY);
+      if (runContext == null) {
+        IssueOutput.error("Could not find run context. Please try again").submit(context);
+        return false;
+      }
+      BlazeAndroidDeviceSelector.DeviceSession deviceSession = env.getCopyableUserData(DEVICE_SESSION_KEY);
+
+      BlazeApkBuildStep buildStep = runContext.getBuildStep();
+      try {
+        return buildStep.build(context, deviceSession);
+      } catch (Exception e) {
+        LOG.error(e);
+        return false;
+      }
+    });
+  }
+
+  private final class BlazeAndroidRunState implements RunProfileState {
+
+    private final Module module;
+    private final ExecutionEnvironment env;
+    private final String launchConfigName;
+    private final BlazeAndroidDeviceSelector.DeviceSession deviceSession;
+    private final BlazeAndroidRunContext runContext;
+    private final LaunchOptions launchOptions;
+
+    private BlazeAndroidRunState(Module module,
+                                 ExecutionEnvironment env,
+                                 String launchConfigName,
+                                 LaunchOptions launchOptions,
+                                 BlazeAndroidDeviceSelector.DeviceSession deviceSession,
+                                 BlazeAndroidRunContext runContext) {
+      this.module = module;
+      this.env = env;
+      this.launchConfigName = launchConfigName;
+      this.deviceSession = deviceSession;
+      this.runContext = runContext;
+      this.launchOptions = launchOptions;
+    }
+
+    @Nullable
+    @Override
+    public ExecutionResult execute(Executor executor, @NotNull ProgramRunner runner) throws ExecutionException {
+      ProcessHandler processHandler;
+      ConsoleView console;
+
+      ApplicationIdProvider applicationIdProvider = runContext.getApplicationIdProvider();
+
+      String applicationId;
+      try {
+        applicationId = applicationIdProvider.getPackageName();
+      }
+      catch (ApkProvisionException e) {
+        throw new ExecutionException("Unable to obtain application id", e);
+      }
+
+      LaunchTasksProvider launchTasksProvider = runContext.getLaunchTasksProvider(launchOptions, debuggerManager);
+
+      DeviceFutures deviceFutures = deviceSession.deviceFutures;
+      assert deviceFutures != null;
+      ProcessHandler previousSessionProcessHandler = deviceSession.sessionInfo != null ?
+                                                     deviceSession.sessionInfo.getProcessHandler() : null;
+
+      if (launchTasksProvider.createsNewProcess()) {
+        // In the case of cold swap, there is an existing process that is connected, but we are going to launch a new one.
+        // Detach the previous process handler so that we don't end up with 2 run tabs for the same launch (the existing one
+        // and the new one).
+        if (previousSessionProcessHandler != null) {
+          previousSessionProcessHandler.detachProcess();
+        }
+
+        processHandler = new AndroidProcessHandler(applicationId, launchTasksProvider.monitorRemoteProcess());
+        console = runContext.getConsoleProvider().createAndAttach(module.getProject(), processHandler, executor);
+      } else {
+        assert previousSessionProcessHandler != null : "No process handler from previous session, yet current tasks don't create one";
+        processHandler = previousSessionProcessHandler;
+        console = null;
+      }
+
+      LaunchInfo launchInfo = new LaunchInfo(executor, runner, env, runContext.getConsoleProvider());
+
+      LaunchTaskRunner task = new LaunchTaskRunner(module.getProject(),
+                                                   launchConfigName,
+                                                   launchInfo,
+                                                   processHandler,
+                                                   deviceSession.deviceFutures,
+                                                   launchTasksProvider);
+      ProgressManager.getInstance().run(task);
+
+      return console == null ? null : new DefaultExecutionResult(console, processHandler);
+    }
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    deployTargetManager.readExternal(element);
+    debuggerManager.readExternal(element);
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    deployTargetManager.writeExternal(element);
+    debuggerManager.writeExternal(element);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunContext.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunContext.java
new file mode 100644
index 0000000..ef6f94d
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeAndroidRunContext.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.runner;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.ApplicationIdProvider;
+import com.android.tools.idea.run.ConsoleProvider;
+import com.android.tools.idea.run.LaunchOptions;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.tasks.DebugConnectorTask;
+import com.android.tools.idea.run.tasks.LaunchTask;
+import com.android.tools.idea.run.tasks.LaunchTasksProvider;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.google.common.collect.ImmutableList;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.runners.ExecutionEnvironment;
+
+import javax.annotation.Nullable;
+import java.util.Set;
+
+/**
+ * Instantiated when the configuration wants to run.
+ */
+public interface BlazeAndroidRunContext {
+
+  BlazeAndroidDeviceSelector getDeviceSelector();
+
+  void augmentEnvironment(ExecutionEnvironment env);
+
+  void augmentLaunchOptions(LaunchOptions.Builder options);
+
+  ConsoleProvider getConsoleProvider();
+
+  BlazeApkBuildStep getBuildStep();
+
+  ApplicationIdProvider getApplicationIdProvider() throws ExecutionException;
+
+  LaunchTasksProvider getLaunchTasksProvider(
+    LaunchOptions launchOptions,
+    BlazeAndroidRunConfigurationDebuggerManager debuggerManager) throws ExecutionException;
+
+  /**
+   * Returns the tasks to deploy the application.
+   */
+  ImmutableList<LaunchTask> getDeployTasks(IDevice device, LaunchOptions launchOptions) throws ExecutionException;
+
+  /**
+   * Returns the task to launch the application.
+   */
+  @Nullable
+  LaunchTask getApplicationLaunchTask(LaunchOptions launchOptions,
+                                      AndroidDebugger androidDebugger,
+                                      AndroidDebuggerState androidDebuggerState,
+                                      ProcessHandlerLaunchStatus processHandlerLaunchStatus) throws ExecutionException;
+
+  /**
+   * Returns the task to connect the debugger.
+   */
+  @Nullable
+  DebugConnectorTask getDebuggerTask(LaunchOptions launchOptions,
+                                     AndroidDebugger androidDebugger,
+                                     AndroidDebuggerState androidDebuggerState,
+                                     Set<String> packageIds) throws ExecutionException;
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStep.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStep.java
new file mode 100644
index 0000000..3667c93
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStep.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.runner;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+
+/**
+ * Builds the APK.
+ */
+public interface BlazeApkBuildStep {
+  /**
+   * Builds an optionally installs the APK.
+   *
+   * @return True to continue the launch.
+   */
+  boolean build(BlazeContext context, BlazeAndroidDeviceSelector.DeviceSession deviceSession);
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.java
new file mode 100644
index 0000000..2da6a9a
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/runner/BlazeApkBuildStepNormalBuild.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.android.run.runner;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.google.idea.blaze.android.run.deployinfo.BlazeApkDeployInfoProtoHelper;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.intellij.execution.ExecutionException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.CancellationException;
+
+/**
+ * Builds the APK using normal blaze build.
+ */
+public class BlazeApkBuildStepNormalBuild implements BlazeApkBuildStep {
+  private final Project project;
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+  private final ImmutableList<String> buildFlags;
+  private final SettableFuture<BlazeAndroidDeployInfo> deployInfoFuture = SettableFuture.create();
+
+  public BlazeApkBuildStepNormalBuild(Project project,
+                                      BlazeAndroidRunConfigurationCommonState commonState,
+                                      ImmutableList<String> buildFlags) {
+    this.project = project;
+    this.commonState = commonState;
+    this.buildFlags = buildFlags;
+  }
+
+  @Override
+  public boolean build(BlazeContext context, BlazeAndroidDeviceSelector.DeviceSession deviceSession) {
+    final ScopedTask buildTask = new ScopedTask(context) {
+      @Override
+      protected void execute(@NotNull BlazeContext context) {
+        BlazeCommand.Builder command = BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD);
+        WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+
+        command
+          .addTargets(commonState.getTarget())
+          .addBlazeFlags("--output_groups=+android_deploy_info")
+          .addBlazeFlags(buildFlags)
+          .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS)
+        ;
+
+        BlazeApkDeployInfoProtoHelper deployInfoHelper = new BlazeApkDeployInfoProtoHelper(project, buildFlags);
+
+        SaveUtil.saveAllFiles();
+        int retVal = ExternalTask.builder(workspaceRoot, command.build())
+          .context(context)
+          .stderr(LineProcessingOutputStream.of(
+            deployInfoHelper.getLineProcessor(),
+            new IssueOutputLineProcessor(project, context, workspaceRoot)
+          ))
+          .build()
+          .run(new LoggedTimingScope(project, Action.BLAZE_BUILD));
+        LocalFileSystem.getInstance().refresh(true);
+
+        if (retVal != 0) {
+          context.setHasError();
+          return;
+        }
+        BlazeAndroidDeployInfo deployInfo = deployInfoHelper.readDeployInfo(context);
+        if (deployInfo == null) {
+          IssueOutput.error("Could not read apk deploy info from build").submit(context);
+          return;
+        }
+        deployInfoFuture.set(deployInfo);
+      }
+    };
+
+    ListenableFuture<Void> buildFuture = BlazeExecutor.submitTask(
+      project,
+      String.format("Executing %s apk build", Blaze.buildSystemName(project)),
+      buildTask
+    );
+
+    try {
+      Futures.get(buildFuture, ExecutionException.class);
+    }
+    catch (ExecutionException e) {
+      context.setHasError();
+    }
+    catch (CancellationException e) {
+      context.setCancelled();
+    }
+    return context.shouldContinue();
+  }
+
+  public ListenableFuture<BlazeAndroidDeployInfo> getDeployInfo() {
+    return deployInfoFuture;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java b/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java
new file mode 100644
index 0000000..878cf08
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/AndroidTestConsoleProvider.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.android.tools.idea.run.ConsoleProvider;
+import com.android.tools.idea.run.testing.AndroidTestConsoleProperties;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.filters.TextConsoleBuilderFactory;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.testframework.sm.SMTestRunnerConnectionUtil;
+import com.intellij.execution.ui.ConsoleView;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Console provider for android_test
+ */
+class AndroidTestConsoleProvider implements ConsoleProvider {
+  private final Project project;
+  private final RunConfiguration runConfiguration;
+  private final BlazeAndroidTestRunConfigurationState configState;
+
+  AndroidTestConsoleProvider(Project project,
+                             RunConfiguration runConfiguration,
+                             BlazeAndroidTestRunConfigurationState configState) {
+    this.project = project;
+    this.runConfiguration = runConfiguration;
+    this.configState = configState;
+  }
+
+  @NotNull
+  @Override
+  public ConsoleView createAndAttach(@NotNull Disposable parent,
+                                     @NotNull ProcessHandler handler,
+                                     @NotNull Executor executor) throws ExecutionException {
+    if (!configState.isRunThroughBlaze()) {
+      return getStockConsoleProvider().createAndAttach(parent, handler, executor);
+    }
+    ConsoleView console = TextConsoleBuilderFactory.getInstance()
+      .createBuilder(project)
+      .getConsole();
+    console.attachToProcess(handler);
+    return console;
+  }
+
+  private ConsoleProvider getStockConsoleProvider() {
+    return (parent, handler, executor) -> {
+      AndroidTestConsoleProperties properties = new AndroidTestConsoleProperties(runConfiguration, executor);
+      ConsoleView consoleView = SMTestRunnerConnectionUtil.createAndAttachConsole("Android", handler, properties);
+      Disposer.register(parent, consoleView);
+      return consoleView;
+    };
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestApplicationIdProvider.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestApplicationIdProvider.java
new file mode 100644
index 0000000..d7e7b86
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestApplicationIdProvider.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.run.test;
+
+import com.android.tools.idea.run.ApkProvisionException;
+import com.android.tools.idea.run.ApplicationIdProvider;
+import com.google.common.collect.Iterables;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.android.run.deployinfo.BlazeAndroidDeployInfo;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
+import org.jetbrains.android.dom.manifest.Manifest;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Application id provider for android_binary.
+ */
+public class BlazeAndroidTestApplicationIdProvider implements ApplicationIdProvider {
+  private final Project project;
+  private final ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture;
+
+  public BlazeAndroidTestApplicationIdProvider(Project project,
+                                               ListenableFuture<BlazeAndroidDeployInfo> deployInfoFuture) {
+    this.project = project;
+    this.deployInfoFuture = deployInfoFuture;
+  }
+
+  @NotNull
+  @Override
+  public String getPackageName() throws ApkProvisionException {
+    BlazeAndroidDeployInfo deployInfo = Futures.get(deployInfoFuture, ApkProvisionException.class);
+    Manifest manifest = Iterables.getFirst(deployInfo.getAdditionalMergedManifests(), null);
+    if (manifest == null) {
+      throw new ApkProvisionException("Could not find manifest for application under test");
+    }
+    String applicationId = ApplicationManager.getApplication().runReadAction(
+      (Computable<String>)() -> manifest.getPackage().getValue()
+    );
+    if (applicationId == null) {
+      throw new ApkProvisionException("No application id in manifest under test");
+    }
+    return applicationId;
+  }
+
+  @Nullable
+  @Override
+  public String getTestPackageName() throws ApkProvisionException {
+    BlazeAndroidDeployInfo deployInfo = Futures.get(deployInfoFuture, ApkProvisionException.class);
+    Manifest manifest = deployInfo.getMergedManifest();
+    if (manifest == null) {
+      throw new ApkProvisionException("Could not find merged manifest: " + deployInfo.getMergedManifestFile());
+    }
+    String applicationId = ApplicationManager.getApplication().runReadAction(
+      (Computable<String>)() -> manifest.getPackage().getValue()
+    );
+    if (applicationId == null) {
+      throw new ApkProvisionException("No application id in merged manifest: " + deployInfo.getMergedManifestFile());
+    }
+    return applicationId;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.java
new file mode 100644
index 0000000..d09065c
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestClassRunConfigurationProducer.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.android.run.test;
+
+import com.android.tools.idea.run.testing.AndroidTestRunConfiguration;
+import com.google.common.base.Strings;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.google.idea.blaze.java.run.producers.BlazeTestRunConfigurationProducer;
+import com.google.idea.blaze.java.run.producers.JUnitConfigurationUtil;
+import com.google.idea.blaze.java.run.producers.ProducerUtils;
+import com.intellij.execution.JavaExecutionUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Producer for run configurations related to Android test classes in Blaze.
+ * <p/>
+ * This class is based on
+ * {@link org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer}.
+ */
+public class BlazeAndroidTestClassRunConfigurationProducer extends BlazeTestRunConfigurationProducer<BlazeAndroidTestRunConfiguration> {
+
+  public BlazeAndroidTestClassRunConfigurationProducer() {
+    super(BlazeAndroidTestRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+    @NotNull BlazeAndroidTestRunConfiguration configuration,
+    @NotNull ConfigurationContext context,
+    @NotNull Ref<PsiElement> sourceElement) {
+
+    final Location contextLocation = context.getLocation();
+    assert contextLocation != null;
+    final Location location = JavaExecutionUtil.stepIntoSingleClass(contextLocation);
+    if (location == null) {
+      return false;
+    }
+
+    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
+      return false;
+    }
+
+    PsiClass testClass = JUnitUtil.getTestClass(location);
+    if (testClass == null) {
+      return false;
+    }
+    sourceElement.set(testClass);
+
+    RuleIdeInfo rule = RunUtil.ruleForTestClass(context.getProject(), testClass, null);
+    if (rule == null) {
+      return false;
+    }
+    if (!rule.kindIsOneOf(Kind.ANDROID_TEST)) {
+      return false;
+    }
+    configuration.setTarget(rule.label);
+    BlazeAndroidTestRunConfigurationState configState = configuration.getConfigState();
+    configState.TESTING_TYPE = AndroidTestRunConfiguration.TEST_CLASS;
+    configState.CLASS_NAME = testClass.getQualifiedName();
+    configuration.setGeneratedName();
+
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+    @NotNull BlazeAndroidTestRunConfiguration configuration,
+    @NotNull ConfigurationContext context) {
+
+    final Location contextLocation = context.getLocation();
+    assert contextLocation != null;
+    final Location location = JavaExecutionUtil.stepIntoSingleClass(contextLocation);
+    if (location == null) {
+      return false;
+    }
+
+    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
+      return false;
+    }
+
+    Location<PsiMethod> methodLocation = ProducerUtils.getMethodLocation(contextLocation);
+    if (methodLocation != null) {
+      return false;
+    }
+
+    PsiClass testClass = JUnitUtil.getTestClass(location);
+    if (testClass == null) {
+      return false;
+    }
+
+    return checkIfAttributesAreTheSame(configuration, testClass);
+  }
+
+  private static boolean checkIfAttributesAreTheSame(
+    BlazeAndroidTestRunConfiguration configuration, PsiClass testClass) {
+    BlazeAndroidTestRunConfigurationState configState = configuration.getConfigState();
+    if (Strings.isNullOrEmpty(configState.CLASS_NAME)) {
+      return false;
+    }
+
+    return configState.TESTING_TYPE == AndroidTestRunConfiguration.TEST_CLASS
+           && configState.CLASS_NAME.equals(testClass.getQualifiedName());
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java
new file mode 100644
index 0000000..1b9d234
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestFilter.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.android.tools.idea.run.testing.AndroidTestRunConfiguration;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A test filter specification for Android tests.
+ */
+final class BlazeAndroidTestFilter {
+  // Specifies that Android tests should be filtered by name.
+  private static final String TEST_FILTER_BY_NAME = BlazeFlags.TEST_ARG + "--test_filter_spec=TEST_NAME";
+  // As part of a name filter spec, the packages to include.
+  private static final String TEST_PACKAGE_NAMES = BlazeFlags.TEST_ARG + "--test_package_names=";
+  // As part of a name filter spec, the classes to include.
+  private static final String TEST_CLASS_NAMES = BlazeFlags.TEST_ARG + "--test_class_names=";
+  // As part of a name filter spec, the full names of methods to run.
+  private static final String TEST_METHOD_NAMES = BlazeFlags.TEST_ARG + "--test_method_full_names=";
+
+  private final int testingType;
+  @Nullable
+  private final String className;
+  @Nullable
+  private final String methodName;
+  @Nullable
+  private final String packageName;
+
+  public BlazeAndroidTestFilter(
+    int testingType,
+    @Nullable String className,
+    @Nullable String methodName,
+    @Nullable String packageName
+  ) {
+    this.testingType = testingType;
+    this.className = className;
+    this.methodName = methodName;
+    this.packageName = packageName;
+  }
+
+  @NotNull
+  public ImmutableList<String> getBlazeFlags() {
+    if (testingType == BlazeAndroidTestRunConfigurationState.TEST_ALL_IN_TARGET) {
+      return ImmutableList.of();
+    }
+    ImmutableList.Builder<String> flags = ImmutableList.builder();
+    flags.add(TEST_FILTER_BY_NAME);
+    switch (testingType) {
+      case AndroidTestRunConfiguration.TEST_ALL_IN_PACKAGE:
+        assert packageName != null;
+        flags.add(TEST_PACKAGE_NAMES + packageName);
+        break;
+      case AndroidTestRunConfiguration.TEST_CLASS:
+        assert className != null;
+        flags.add(TEST_CLASS_NAMES + className);
+        break;
+      case AndroidTestRunConfiguration.TEST_METHOD:
+        assert className != null;
+        assert methodName != null;
+        flags.add(TEST_METHOD_NAMES + className + "#" + methodName);
+        break;
+      default:
+        assert false : "Unknown testing type.";
+    }
+    return flags.build();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java
new file mode 100644
index 0000000..f7d0a9e
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestLaunchTask.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.ConsolePrinter;
+import com.android.tools.idea.run.tasks.LaunchTask;
+import com.android.tools.idea.run.tasks.LaunchTaskDurations;
+import com.android.tools.idea.run.util.LaunchStatus;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.ScopedFunction;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.process.ProcessAdapter;
+import com.intellij.execution.process.ProcessEvent;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.ide.PooledThreadExecutor;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * An Android application launcher that invokes `blaze test` on an android_test target, and sets
+ * up process handling and debugging for the test run.
+ */
+class BlazeAndroidTestLaunchTask implements LaunchTask {
+  // Uses a local device/emulator attached to adb to run an android_test.
+  public static final String TEST_LOCAL_DEVICE = BlazeFlags.TEST_ARG + "--device_broker_type=LOCAL_ADB_SERVER";
+  // Uses a local device/emulator attached to adb to run an android_test.
+  public static final String TEST_DEBUG = BlazeFlags.TEST_ARG + "--enable_debug";
+  // Specifies the serial number for a local test device.
+  private static final String TEST_DEVICE_SERIAL = "--device_serial_number=";
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidTestLaunchTask.class);
+
+  private final Project project;
+  private final Label target;
+  private final List<String> buildFlags;
+  private final BlazeAndroidTestFilter testFilter;
+
+  private ListenableFuture<Boolean> blazeResult;
+
+  private final BlazeAndroidTestRunContext runContext;
+
+  private final boolean debug;
+
+  public BlazeAndroidTestLaunchTask(
+    Project project,
+    Label target,
+    List<String> buildFlags,
+    BlazeAndroidTestFilter testFilter,
+    BlazeAndroidTestRunContext runContext,
+    boolean debug
+  ) {
+    this.project = project;
+    this.target = target;
+    this.buildFlags = buildFlags;
+    this.testFilter = testFilter;
+    this.runContext = runContext;
+    this.debug = debug;
+  }
+
+  @NotNull
+  @Override
+  public String getDescription() {
+    return String.format("Running %s tests", Blaze.buildSystemName(project));
+  }
+
+  @Override
+  public int getDuration() {
+    return LaunchTaskDurations.LAUNCH_ACTIVITY;
+  }
+
+  @Override
+  public boolean perform(@NotNull IDevice device, @NotNull LaunchStatus launchStatus, @NotNull ConsolePrinter printer) {
+    BlazeExecutor executor = BlazeExecutor.getInstance();
+
+    ProcessHandlerLaunchStatus processHandlerLaunchStatus = (ProcessHandlerLaunchStatus)launchStatus;
+    final ProcessHandler processHandler = processHandlerLaunchStatus.getProcessHandler();
+
+    blazeResult = executor.submit(new Callable<Boolean>() {
+      @Override
+      public Boolean call() throws Exception {
+        return Scope.root(new ScopedFunction<Boolean>() {
+          @Override
+          public Boolean execute(@NotNull BlazeContext context) {
+            ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+            if (projectViewSet == null) {
+              IssueOutput.error("Could not load project view. Please resync project.").submit(context);
+              return false;
+            }
+
+            BlazeCommand.Builder commandBuilder = BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.TEST)
+              .addTargets(target);
+            // Build flags must match BlazeBeforeRunTask.
+            commandBuilder
+              .addBlazeFlags(buildFlags);
+            // Run the test on the selected local device/emulator.
+            commandBuilder
+              .addBlazeFlags(TEST_LOCAL_DEVICE, BlazeFlags.TEST_OUTPUT_STREAMED)
+              .addBlazeFlags(testDeviceSerialFlags(device.getSerialNumber()))
+              .addBlazeFlags(testFilter.getBlazeFlags());
+            if (debug) {
+              commandBuilder.addBlazeFlags(TEST_DEBUG, BlazeFlags.NO_CACHE_TEST_RESULTS);
+            }
+            BlazeCommand command = commandBuilder.build();
+
+            printer.stdout(String.format("Starting %s test...\n", Blaze.buildSystemName(project)));
+            printer.stdout(command + "\n");
+            LineProcessingOutputStream.LineProcessor stdoutLineProcessor = line -> {
+              printer.stdout(line);
+              return true;
+            };
+            LineProcessingOutputStream.LineProcessor stderrLineProcessor = line -> {
+              printer.stderr(line);
+              return true;
+            };
+            int retVal = ExternalTask.builder(WorkspaceRoot.fromProject(project), command)
+              .context(context)
+              .stdout(LineProcessingOutputStream.of(stdoutLineProcessor))
+              .stderr(LineProcessingOutputStream.of(stderrLineProcessor))
+              .build()
+              .run();
+
+            if (retVal != 0) {
+              context.setHasError();
+            }
+
+            return !context.hasErrors();
+          }
+        });
+      }
+    });
+
+    blazeResult.addListener(runContext::onLaunchTaskComplete, PooledThreadExecutor.INSTANCE);
+
+    // The debug case is set up in ConnectBlazeTestDebuggerTask
+    if (!debug) {
+      waitAndSetUpForKillingBlazeOnStop(processHandler, launchStatus);
+    }
+    return true;
+  }
+
+  /**
+   *  Hooks up the Blaze process to be killed if the user hits the 'Stop' button, then waits for
+   *  the Blaze process to stop.
+   *  In non-debug mode, we wait for test execution to finish before returning from launch()
+   *  (this matches the behavior of the stock ddmlib runner).
+   */
+  private void waitAndSetUpForKillingBlazeOnStop(@NotNull final ProcessHandler processHandler, @NotNull LaunchStatus launchStatus) {
+    processHandler.addProcessListener(new ProcessAdapter() {
+      @Override
+      public void processWillTerminate(ProcessEvent event, boolean willBeDestroyed) {
+        blazeResult.cancel(true /* mayInterruptIfRunning */);
+        launchStatus.terminateLaunch("Test run stopped.\n");
+      }
+    });
+
+    try {
+      blazeResult.get();
+      launchStatus.terminateLaunch("Tests ran to completion.\n");
+    }
+    catch (CancellationException e) {
+      // The user has canceled the test.
+      launchStatus.terminateLaunch("Test run stopped.\n");
+    }
+    catch (InterruptedException e) {
+      // We've been interrupted - cancel the underlying Blaze process.
+      blazeResult.cancel(true /* mayInterruptIfRunning */);
+      launchStatus.terminateLaunch("Test run stopped.\n");
+    }
+    catch (ExecutionException e) {
+      LOG.error(e);
+      launchStatus.terminateLaunch("Test run stopped due to internal exception. Please file a bug report.\n");
+    }
+  }
+
+  @NotNull
+  private static String testDeviceSerialFlags(@NotNull String serial) {
+    return BlazeFlags.TEST_ARG + TEST_DEVICE_SERIAL + serial;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.java
new file mode 100644
index 0000000..8de6656
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestMethodRunConfigurationProducer.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.android.run.test;
+
+import com.android.tools.idea.run.testing.AndroidTestRunConfiguration;
+import com.google.common.base.Strings;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.google.idea.blaze.java.run.producers.BlazeTestRunConfigurationProducer;
+import com.google.idea.blaze.java.run.producers.JUnitConfigurationUtil;
+import com.google.idea.blaze.java.run.producers.ProducerUtils;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Producer for run configurations related to Android test methods in Blaze.
+ * <p/>
+ * This class is based on
+ * {@link org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer}.
+ */
+public class BlazeAndroidTestMethodRunConfigurationProducer extends BlazeTestRunConfigurationProducer<BlazeAndroidTestRunConfiguration> {
+
+  public BlazeAndroidTestMethodRunConfigurationProducer() {
+    super(BlazeAndroidTestRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+    @NotNull BlazeAndroidTestRunConfiguration configuration,
+    @NotNull ConfigurationContext context,
+    @NotNull Ref<PsiElement> sourceElement) {
+
+    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
+      return false;
+    }
+
+    final Location contextLocation = context.getLocation();
+    assert contextLocation != null;
+    Location<PsiMethod> methodLocation = ProducerUtils.getMethodLocation(contextLocation);
+    if (methodLocation == null) {
+      return false;
+    }
+
+    final PsiMethod psiMethod = methodLocation.getPsiElement();
+    sourceElement.set(psiMethod);
+
+    final PsiClass containingClass = psiMethod.getContainingClass();
+    if (containingClass == null) {
+      return false;
+    }
+
+    RuleIdeInfo rule = RunUtil.ruleForTestClass(context.getProject(), containingClass, null);
+    if (rule == null) {
+      return false;
+    }
+    if (!rule.kindIsOneOf(Kind.ANDROID_TEST)) {
+      return false;
+    }
+    configuration.setTarget(rule.label);
+    BlazeAndroidTestRunConfigurationState configState = configuration.getConfigState();
+    configState.TESTING_TYPE = AndroidTestRunConfiguration.TEST_METHOD;
+    configState.CLASS_NAME = containingClass.getQualifiedName();
+    configState.METHOD_NAME = psiMethod.getName();
+    configuration.setGeneratedName();
+
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+    @NotNull BlazeAndroidTestRunConfiguration configuration,
+    @NotNull ConfigurationContext context) {
+
+    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
+      return false;
+    }
+
+    final Location contextLocation = context.getLocation();
+    assert contextLocation != null;
+
+    Location<PsiMethod> methodLocation = ProducerUtils.getMethodLocation(contextLocation);
+    if (methodLocation == null) {
+      return false;
+    }
+
+    final PsiMethod psiMethod = methodLocation.getPsiElement();
+    final PsiClass containingClass = psiMethod.getContainingClass();
+    if (containingClass == null) {
+      return false;
+    }
+
+    return checkIfAttributesAreTheSame(configuration, psiMethod);
+  }
+
+  private static boolean checkIfAttributesAreTheSame(BlazeAndroidTestRunConfiguration configuration,
+                                                     PsiMethod testMethod) {
+    BlazeAndroidTestRunConfigurationState configState = configuration.getConfigState();
+    if (Strings.isNullOrEmpty(configState.CLASS_NAME)
+        || Strings.isNullOrEmpty(configState.METHOD_NAME)) {
+      return false;
+    }
+
+    return configState.TESTING_TYPE == AndroidTestRunConfiguration.TEST_METHOD
+           && configState.CLASS_NAME.equals(testMethod.getContainingClass().getQualifiedName())
+           && configState.METHOD_NAME.equals(testMethod.getName());
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfiguration.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfiguration.java
new file mode 100644
index 0000000..1e01d20
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfiguration.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.android.tools.idea.run.ValidationError;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfiguration;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunConfigurationRunner;
+import com.google.idea.blaze.android.run.runner.BlazeAndroidRunContext;
+import com.google.idea.blaze.android.sync.projectstructure.BlazeAndroidProjectStructureSyncer;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.JavaExecutionUtil;
+import com.intellij.execution.configurations.*;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.options.SettingsEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Comparing;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * An extension of the normal Android Studio run configuration for launching Android tests,
+ * adapted specifically for selecting and launching android_test targets.
+ */
+public final class BlazeAndroidTestRunConfiguration extends LocatableConfigurationBase
+  implements BlazeAndroidRunConfiguration, RunConfiguration {
+
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidTestRunConfiguration.class);
+  private final Project project;
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+  private final BlazeAndroidRunConfigurationRunner runner;
+
+  @NotNull private final BlazeAndroidTestRunConfigurationState configState = new BlazeAndroidTestRunConfigurationState();
+
+  public BlazeAndroidTestRunConfiguration(Project project, ConfigurationFactory factory) {
+    super(project, factory, "");
+    this.project = project;
+
+    RuleIdeInfo rule = RuleFinder.getInstance().firstRuleOfKinds(project, Kind.ANDROID_TEST);
+    this.commonState = new BlazeAndroidRunConfigurationCommonState(rule != null ? rule.label : null, ImmutableList.of());
+    this.runner = new BlazeAndroidRunConfigurationRunner(project, this, commonState, true, getUniqueID());
+  }
+
+  @Override
+  public BlazeAndroidRunConfigurationCommonState getCommonState() {
+    return commonState;
+  }
+
+  @NotNull
+  public BlazeAndroidTestRunConfigurationState getConfigState() {
+    return configState;
+  }
+
+  @Nullable
+  @Override
+  public final Label getTarget() {
+    return commonState.getTarget();
+  }
+
+  public final void setTarget(@Nullable Label target) {
+    commonState.setTarget(target);
+  }
+
+  @Override
+  public BlazeAndroidRunConfigurationRunner getRunner() {
+    return runner;
+  }
+
+  @Override
+  public BlazeAndroidRunContext createRunContext(Project project,
+                                                 AndroidFacet facet,
+                                                 ExecutionEnvironment env,
+                                                 ImmutableList<String> buildFlags) {
+    return new BlazeAndroidTestRunContext(project, facet, this, env, commonState, configState, buildFlags);
+  }
+
+  @NotNull
+  @Override
+  public SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
+    return new BlazeAndroidTestRunConfigurationEditor(
+      getProject(),
+      new BlazeAndroidTestRunConfigurationStateEditor(project)
+    );
+  }
+
+  @Override
+  public final void checkConfiguration() throws RuntimeConfigurationException {
+    List<ValidationError> errors = validate();
+    if (errors.isEmpty()) {
+      return;
+    }
+    ValidationError topError = Ordering.natural().max(errors);
+    if (topError.isFatal()) {
+      throw new RuntimeConfigurationError(topError.getMessage(), topError.getQuickfix());
+    }
+    throw new RuntimeConfigurationWarning(topError.getMessage(), topError.getQuickfix());
+  }
+
+  private List<ValidationError> validate() {
+    List<ValidationError> errors = Lists.newArrayList();
+    errors.addAll(runner.validate(getModule()));
+    commonState.checkConfiguration(getProject(), Kind.ANDROID_TEST, errors);
+    return errors;
+  }
+
+  @Override
+  @Nullable
+  public final RunProfileState getState(@NotNull final Executor executor, @NotNull ExecutionEnvironment env) throws ExecutionException {
+    final Module module = getModule();
+    return runner.getState(module, executor, env);
+  }
+
+  private Module getModule() {
+    return BlazeAndroidProjectStructureSyncer.ensureRunConfigurationModule(project, getTarget());
+  }
+
+  @Override
+  @Nullable
+  public String suggestedName() {
+    Label target = commonState.getTarget();
+    if (target == null) {
+      return null;
+    }
+    String name = target.ruleName().toString();
+    if (configState.TESTING_TYPE == BlazeAndroidTestRunConfigurationState.TEST_CLASS) {
+      name += ": " + configState.CLASS_NAME;
+    }
+    else if (configState.TESTING_TYPE == BlazeAndroidTestRunConfigurationState.TEST_METHOD) {
+      name += ": " + configState.CLASS_NAME + "#" + configState.METHOD_NAME;
+    }
+    return String.format("%s test: %s", Blaze.buildSystemName(project), name);
+  }
+
+  @Override
+  public boolean isGeneratedName() {
+    final String name = getName();
+
+    if ((configState.TESTING_TYPE == BlazeAndroidTestRunConfigurationState.TEST_CLASS || configState.TESTING_TYPE == BlazeAndroidTestRunConfigurationState.TEST_METHOD) &&
+        (configState.CLASS_NAME == null || configState.CLASS_NAME.length() == 0)) {
+      return JavaExecutionUtil.isNewName(name);
+    }
+    if (configState.TESTING_TYPE == BlazeAndroidTestRunConfigurationState.TEST_METHOD &&
+        (configState.METHOD_NAME == null || configState.METHOD_NAME.length() == 0)) {
+      return JavaExecutionUtil.isNewName(name);
+    }
+    return Comparing.equal(name, suggestedName());
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    super.readExternal(element);
+
+    commonState.readExternal(element);
+    runner.readExternal(element);;
+    configState.readExternal(element);
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    super.writeExternal(element);
+
+    commonState.writeExternal(element);
+    runner.writeExternal(element);;
+    configState.writeExternal(element);
+  }
+
+  @Override
+  public RunConfiguration clone() {
+    final Element element = new Element("dummy");
+    try {
+      writeExternal(element);
+      BlazeAndroidTestRunConfiguration clone = new BlazeAndroidTestRunConfiguration(
+        getProject(), getFactory());
+      clone.readExternal(element);
+      return clone;
+    } catch (InvalidDataException e) {
+      LOG.error(e);
+      return null;
+    } catch (WriteExternalException e) {
+      LOG.error(e);
+      return null;
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationEditor.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationEditor.java
new file mode 100644
index 0000000..34f3fcb
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationEditor.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonStateEditor;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.options.SettingsEditor;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.util.List;
+
+/**
+ * A simplified, Blaze-specific variant of
+ * {@link org.jetbrains.android.run.AndroidRunConfigurationEditor}.
+ */
+class BlazeAndroidTestRunConfigurationEditor extends SettingsEditor<BlazeAndroidTestRunConfiguration> {
+
+  private final BlazeAndroidTestRunConfigurationStateEditor kindSpecificEditor;
+  private final BlazeAndroidRunConfigurationCommonStateEditor commonStateEditor;
+
+  public BlazeAndroidTestRunConfigurationEditor(
+    Project project,
+    BlazeAndroidTestRunConfigurationStateEditor kindSpecificEditor) {
+    this.kindSpecificEditor = kindSpecificEditor;
+    this.commonStateEditor = new BlazeAndroidRunConfigurationCommonStateEditor(project, Kind.ANDROID_TEST);
+  }
+
+  @Override
+  protected void resetEditorFrom(BlazeAndroidTestRunConfiguration configuration) {
+    commonStateEditor.resetEditorFrom(configuration.getCommonState());
+    kindSpecificEditor.resetFrom(configuration);
+  }
+
+  @Override
+  protected void applyEditorTo(@NotNull BlazeAndroidTestRunConfiguration configuration)
+    throws ConfigurationException {
+    commonStateEditor.applyEditorTo(configuration.getCommonState());
+    kindSpecificEditor.applyTo(configuration);
+  }
+
+  @Override
+  @NotNull
+  protected JComponent createEditor() {
+    List<Component> components = Lists.newArrayList();
+    components.addAll(commonStateEditor.getComponents());
+    components.add(kindSpecificEditor.getComponent());
+    return UiUtil.createBox(components);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java
new file mode 100644
index 0000000..91236aa
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationState.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.intellij.openapi.util.DefaultJDOMExternalizer;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.JDOMExternalizable;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+import org.jetbrains.annotations.Contract;
+
+/**
+ * State specific for the android test configuration.
+ */
+final class BlazeAndroidTestRunConfigurationState implements JDOMExternalizable {
+
+  private static final String RUN_THROUGH_BLAZE_ATTR = "blaze-run-through-blaze";
+
+  public static final int TEST_ALL_IN_MODULE = 0;
+  public static final int TEST_ALL_IN_PACKAGE = 1;
+  public static final int TEST_CLASS = 2;
+  public static final int TEST_METHOD = 3;
+
+  // We reinterpret Android Studio's test mode for running "all tests in a module" (all the tests in the installed test APK) as running all
+  // the tests in a rule.
+  public static final int TEST_ALL_IN_TARGET = TEST_ALL_IN_MODULE;
+
+  public int TESTING_TYPE = TEST_ALL_IN_MODULE;
+  public String INSTRUMENTATION_RUNNER_CLASS = InstrumentationRunnerProvider.getDefaultInstrumentationRunnerClass();
+  public String METHOD_NAME = "";
+  public String CLASS_NAME = "";
+  public String PACKAGE_NAME = "";
+  public String EXTRA_OPTIONS = "";
+
+  // Whether to delegate to 'blaze test'.
+  private boolean runThroughBlaze;
+
+  @Contract(pure = true)
+  boolean isRunThroughBlaze() {
+    return runThroughBlaze;
+  }
+
+  void setRunThroughBlaze(boolean runThroughBlaze) {
+    this.runThroughBlaze = runThroughBlaze;
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    DefaultJDOMExternalizer.readExternal(this, element);
+
+    runThroughBlaze = Boolean.parseBoolean(element.getAttributeValue(RUN_THROUGH_BLAZE_ATTR));
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    DefaultJDOMExternalizer.writeExternal(this, element);
+
+    element.setAttribute(RUN_THROUGH_BLAZE_ATTR, Boolean.toString(runThroughBlaze));
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java
new file mode 100644
index 0000000..265bf2f
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationStateEditor.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.idea.blaze.android.run.test;
+
+import com.android.tools.idea.run.ConfigurationSpecificEditor;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.LabeledComponent;
+import com.intellij.ui.EditorTextField;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.uiDesigner.core.GridConstraints;
+import com.intellij.uiDesigner.core.GridLayoutManager;
+import com.intellij.uiDesigner.core.Spacer;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.ResourceBundle;
+
+import static com.android.tools.idea.run.testing.AndroidTestRunConfiguration.*;
+
+
+/**
+ * The part of the Blaze Android test configuration editor that allows the user to pick an
+ * android_test target and test filters.
+ * Forked from {@link org.jetbrains.android.run.testing.TestRunParameters}.
+ */
+class BlazeAndroidTestRunConfigurationStateEditor implements ConfigurationSpecificEditor<BlazeAndroidTestRunConfiguration> {
+  private JRadioButton allInPackageButton;
+  private JRadioButton classButton;
+  private JRadioButton testMethodButton;
+  private JRadioButton allInTargetButton;
+  private LabeledComponent<EditorTextField> packageComponent;
+  private LabeledComponent<EditorTextField> classComponent;
+  private LabeledComponent<EditorTextField> methodComponent;
+  private JPanel panel;
+  private LabeledComponent<EditorTextField> runnerComponent;
+  private JBLabel labelTest;
+  private JCheckBox runThroughBlazeTestCheckBox;
+  private final JRadioButton[] testingType2RadioButton = new JRadioButton[4];
+
+  @NotNull
+  private JComponent anchor;
+
+  BlazeAndroidTestRunConfigurationStateEditor(Project project) {
+    setupUI(project);
+
+    packageComponent.setComponent(new EditorTextField());
+
+    classComponent.setComponent(new EditorTextField());
+
+    runnerComponent.setComponent(new EditorTextField());
+
+    methodComponent.setComponent(new EditorTextField());
+
+    addTestingType(BlazeAndroidTestRunConfigurationState.TEST_ALL_IN_TARGET, allInTargetButton);
+    addTestingType(TEST_ALL_IN_PACKAGE, allInPackageButton);
+    addTestingType(TEST_CLASS, classButton);
+    addTestingType(TEST_METHOD, testMethodButton);
+
+    setAnchor(packageComponent.getLabel());
+  }
+
+  private void addTestingType(final int type, JRadioButton button) {
+    testingType2RadioButton[type] = button;
+    button.addActionListener(new ActionListener() {
+      @Override
+      public void actionPerformed(ActionEvent e) {
+        updateLabelComponents(type);
+      }
+    });
+  }
+
+  @Override
+  public JComponent getAnchor() {
+    return anchor;
+  }
+
+  @Override
+  public void setAnchor(JComponent anchor) {
+    this.anchor = anchor;
+    packageComponent.setAnchor(anchor);
+    classComponent.setAnchor(anchor);
+    methodComponent.setAnchor(anchor);
+    labelTest.setAnchor(anchor);
+  }
+
+  private void updateButtonsAndLabelComponents(int type) {
+    allInTargetButton.setSelected(type == TEST_ALL_IN_MODULE);
+    allInPackageButton.setSelected(type == TEST_ALL_IN_PACKAGE);
+    classButton.setSelected(type == TEST_CLASS);
+    testMethodButton.setSelected(type == TEST_METHOD);
+    updateLabelComponents(type);
+  }
+
+  private void updateLabelComponents(int type) {
+    packageComponent.setVisible(type == TEST_ALL_IN_PACKAGE);
+    classComponent.setVisible(type == TEST_CLASS || type == TEST_METHOD);
+    methodComponent.setVisible(type == TEST_METHOD);
+  }
+
+  private void setupUI(Project project)
+  {
+    try {
+      doSetupUI(project);
+    }
+    catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
+      // Can't happen - this is from IJ generated code
+    }
+  }
+
+  /**
+   * Initially generated by IntelliJ from a .form file, then checked in.
+   */
+  private void doSetupUI(Project project)
+    throws ClassCastException, InstantiationException, IllegalAccessException, ClassNotFoundException {
+    panel = new JPanel();
+    panel.setLayout(new GridLayoutManager(6, 6, new Insets(0, 0, 0, 0), -1, -1));
+    panel.setAlignmentX(0.0f);
+    allInPackageButton = new JRadioButton();
+    allInPackageButton
+      .setActionCommand(ResourceBundle.getBundle("messages/ExecutionBundle").getString("jnit.configuration.all.tests.in.package.radio"));
+    this.loadButtonText(allInPackageButton, ResourceBundle.getBundle("messages/AndroidBundle")
+      .getString("android.run.configuration.all.in.package.radio"));
+    panel.add(allInPackageButton,
+              new GridConstraints(1, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    classButton = new JRadioButton();
+    classButton.setActionCommand(ResourceBundle.getBundle("messages/ExecutionBundle").getString("junit.configuration.test.class.radio"));
+    classButton.setEnabled(true);
+    classButton.setSelected(false);
+    this.loadButtonText(classButton,
+                        ResourceBundle.getBundle("messages/AndroidBundle").getString("android.run.configuration.class.radio"));
+    panel.add(classButton,
+              new GridConstraints(1, 3, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    testMethodButton = new JRadioButton();
+    testMethodButton
+      .setActionCommand(ResourceBundle.getBundle("messages/ExecutionBundle").getString("junit.configuration.test.method.radio"));
+    testMethodButton.setSelected(false);
+    this.loadButtonText(testMethodButton,
+                        ResourceBundle.getBundle("messages/AndroidBundle").getString("android.run.configuration.method.radio"));
+    panel.add(testMethodButton,
+              new GridConstraints(1, 4, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    labelTest = new JBLabel();
+    labelTest.setHorizontalAlignment(2);
+    labelTest.setHorizontalTextPosition(2);
+    labelTest.setIconTextGap(4);
+    this.loadLabelText(labelTest, ResourceBundle.getBundle("messages/ExecutionBundle")
+      .getString("junit.configuration.configure.junit.test.label"));
+    panel.add(labelTest,
+              new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED,
+                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    final Spacer spacer1 = new Spacer();
+    panel.add(spacer1, new GridConstraints(1, 5, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                           GridConstraints.SIZEPOLICY_WANT_GROW, 1, null, null, null, 0, false));
+    allInTargetButton = new JRadioButton();
+    allInTargetButton.setText("All in test target");
+    allInTargetButton.setMnemonic('A');
+    allInTargetButton.setDisplayedMnemonicIndex(0);
+    panel.add(allInTargetButton, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
+                                                         GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                     GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    classComponent = new LabeledComponent();
+    classComponent.setComponentClass("javax.swing.JPanel");
+    classComponent.setLabelLocation("West");
+    classComponent.setText(ResourceBundle.getBundle("messages/AndroidBundle").getString("android.run.configuration.class.label"));
+    panel.add(classComponent, new GridConstraints(3, 0, 1, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                                      GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                  GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    methodComponent = new LabeledComponent();
+    methodComponent.setComponentClass("com.intellij.openapi.ui.TextFieldWithBrowseButton$NoPathCompletion");
+    methodComponent.setLabelLocation("West");
+    methodComponent.setText(ResourceBundle.getBundle("messages/AndroidBundle").getString("android.run.configuration.method.label"));
+    panel.add(methodComponent, new GridConstraints(4, 0, 1, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                                       GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                   GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    runnerComponent = new LabeledComponent();
+    runnerComponent.setComponentClass("javax.swing.JPanel");
+    runnerComponent.setEnabled(true);
+    runnerComponent.setLabelLocation("North");
+    runnerComponent
+      .setText(ResourceBundle.getBundle("messages/AndroidBundle").getString("android.test.run.configuration.instrumentation.label"));
+    panel.add(runnerComponent, new GridConstraints(5, 0, 1, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                                       GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                   GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    packageComponent = new LabeledComponent();
+    packageComponent.setComponentClass("javax.swing.JPanel");
+    packageComponent.setLabelLocation("West");
+    packageComponent.setText(ResourceBundle.getBundle("messages/AndroidBundle").getString("android.run.configuration.package.label"));
+    panel.add(packageComponent, new GridConstraints(2, 0, 1, 6, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                                        GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                    GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    runThroughBlazeTestCheckBox = new JCheckBox();
+    runThroughBlazeTestCheckBox.setText(String.format("Run through '%s test'", Blaze.buildSystemName(project).toLowerCase()));
+    runThroughBlazeTestCheckBox.setToolTipText(String.format("Slower, but more truthful to %s", Blaze.buildSystemName(project)));
+    panel.add(runThroughBlazeTestCheckBox, new GridConstraints(0, 0, 1, 6, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE,
+                                                                   GridConstraints.SIZEPOLICY_CAN_SHRINK |
+                                                                   GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED,
+                                                               null, null, null, 0, false));
+    ButtonGroup buttonGroup;
+    buttonGroup = new ButtonGroup();
+    buttonGroup.add(allInPackageButton);
+    buttonGroup.add(classButton);
+    buttonGroup.add(testMethodButton);
+    buttonGroup.add(allInTargetButton);
+  }
+
+  /**
+   * Initially generated by IntelliJ from a .form file, then checked in.
+   */
+  private void loadLabelText(JLabel component, String text) {
+    StringBuffer result = new StringBuffer();
+    boolean haveMnemonic = false;
+    char mnemonic = '\0';
+    int mnemonicIndex = -1;
+    for (int i = 0; i < text.length(); i++) {
+      if (text.charAt(i) == '&') {
+        i++;
+        if (i == text.length()) break;
+        if (!haveMnemonic && text.charAt(i) != '&') {
+          haveMnemonic = true;
+          mnemonic = text.charAt(i);
+          mnemonicIndex = result.length();
+        }
+      }
+      result.append(text.charAt(i));
+    }
+    component.setText(result.toString());
+    if (haveMnemonic) {
+      component.setDisplayedMnemonic(mnemonic);
+      component.setDisplayedMnemonicIndex(mnemonicIndex);
+    }
+  }
+
+  /**
+   * Initially generated by IntelliJ from a .form file and checked in.
+   */
+  private void loadButtonText(AbstractButton component, String text) {
+    StringBuffer result = new StringBuffer();
+    boolean haveMnemonic = false;
+    char mnemonic = '\0';
+    int mnemonicIndex = -1;
+    for (int i = 0; i < text.length(); i++) {
+      if (text.charAt(i) == '&') {
+        i++;
+        if (i == text.length()) break;
+        if (!haveMnemonic && text.charAt(i) != '&') {
+          haveMnemonic = true;
+          mnemonic = text.charAt(i);
+          mnemonicIndex = result.length();
+        }
+      }
+      result.append(text.charAt(i));
+    }
+    component.setText(result.toString());
+    if (haveMnemonic) {
+      component.setMnemonic(mnemonic);
+      component.setDisplayedMnemonicIndex(mnemonicIndex);
+    }
+  }
+
+  private int getTestingType() {
+    for (int i = 0, myTestingType2RadioButtonLength = testingType2RadioButton.length;
+         i < myTestingType2RadioButtonLength; i++) {
+      JRadioButton button = testingType2RadioButton[i];
+      if (button.isSelected()) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  @Override
+  public void applyTo(BlazeAndroidTestRunConfiguration configuration) {
+    BlazeAndroidTestRunConfigurationState configState = configuration.getConfigState();
+    configState.setRunThroughBlaze(runThroughBlazeTestCheckBox.isSelected());
+
+    configState.TESTING_TYPE = getTestingType();
+    configState.CLASS_NAME = classComponent.getComponent().getText();
+    configState.METHOD_NAME = methodComponent.getComponent().getText();
+    configState.PACKAGE_NAME = packageComponent.getComponent().getText();
+    configState.INSTRUMENTATION_RUNNER_CLASS = runnerComponent.getComponent().getText();
+  }
+
+  @Override
+  public void resetFrom(BlazeAndroidTestRunConfiguration configuration) {
+    BlazeAndroidTestRunConfigurationState configState = configuration.getConfigState();
+    runThroughBlazeTestCheckBox.setSelected(configState.isRunThroughBlaze());
+
+    updateButtonsAndLabelComponents(configState.TESTING_TYPE);
+    packageComponent.getComponent().setText(configState.PACKAGE_NAME);
+    classComponent.getComponent().setText(configState.CLASS_NAME);
+    methodComponent.getComponent().setText(configState.METHOD_NAME);
+    runnerComponent.getComponent().setText(configState.INSTRUMENTATION_RUNNER_CLASS);
+  }
+
+  @Override
+  public Component getComponent() {
+    return panel;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationType.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationType.java
new file mode 100644
index 0000000..acc06d9
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunConfigurationType.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.google.idea.blaze.android.run.BlazeBeforeRunTaskProvider;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.execution.BeforeRunTask;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.ConfigurationType;
+import com.intellij.execution.configurations.ConfigurationTypeUtil;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import com.intellij.ui.LayeredIcon;
+import icons.AndroidIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+/**
+ * A type for Android test run configurations adapted specifically to run android_test targets.
+ */
+public class BlazeAndroidTestRunConfigurationType implements ConfigurationType {
+  private static final Icon ANDROID_TEST_ICON;
+
+  static {
+    LayeredIcon icon = new LayeredIcon(2);
+    icon.setIcon(AndroidIcons.Android, 0);
+    icon.setIcon(AllIcons.Nodes.JunitTestMark, 1);
+    ANDROID_TEST_ICON = icon;
+  }
+
+  private final BlazeAndroidTestRunConfigurationFactory factory =
+    new BlazeAndroidTestRunConfigurationFactory(this);
+
+  public static class BlazeAndroidTestRuleConfigurationFactory implements BlazeRuleConfigurationFactory {
+    @Override
+    public boolean handlesRule(WorkspaceLanguageSettings workspaceLanguageSettings, @NotNull RuleIdeInfo rule) {
+      return rule.kindIsOneOf(Kind.ANDROID_TEST);
+    }
+
+    @Override
+    @NotNull
+    public RunnerAndConfigurationSettings createForRule(@NotNull RunManager runManager, @NotNull RuleIdeInfo rule) {
+      return getInstance().factory.createForRule(runManager, rule);
+    }
+  }
+
+  public static class BlazeAndroidTestRunConfigurationFactory extends ConfigurationFactory {
+
+    protected BlazeAndroidTestRunConfigurationFactory(@NotNull ConfigurationType type) {
+      super(type);
+    }
+
+    @Override
+    @NotNull
+    public BlazeAndroidTestRunConfiguration createTemplateConfiguration(@NotNull Project project) {
+      return new BlazeAndroidTestRunConfiguration(project, this);
+    }
+
+    @Override
+    public boolean canConfigurationBeSingleton() {
+      return false;
+    }
+
+    @Override
+    public boolean isApplicable(@NotNull Project project) {
+      return Blaze.isBlazeProject(project);
+    }
+
+    @Override
+    public void configureBeforeRunTaskDefaults(
+      Key<? extends BeforeRunTask> providerID, BeforeRunTask task) {
+      task.setEnabled(providerID.equals(BlazeBeforeRunTaskProvider.ID));
+    }
+
+    @NotNull
+    public RunnerAndConfigurationSettings createForRule(@NotNull RunManager runManager, @NotNull RuleIdeInfo rule) {
+      final RunnerAndConfigurationSettings settings =
+        runManager.createRunConfiguration(rule.label.toString(), this);
+      final BlazeAndroidTestRunConfiguration configuration =
+        (BlazeAndroidTestRunConfiguration) settings.getConfiguration();
+      configuration.setTarget(rule.label);
+      return settings;
+    }
+
+    @Override
+    public boolean isConfigurationSingletonByDefault() {
+      return true;
+    }
+  }
+
+  public static BlazeAndroidTestRunConfigurationType getInstance() {
+    return ConfigurationTypeUtil.findConfigurationType(BlazeAndroidTestRunConfigurationType.class);
+  }
+
+  @Override
+  public String getDisplayName() {
+    return Blaze.defaultBuildSystemName() + " Android Test";
+  }
+
+  @Override
+  public String getConfigurationTypeDescription() {
+    return "Launch/debug configuration for android_test rules";
+  }
+
+  @Override
+  public Icon getIcon() {
+    return ANDROID_TEST_ICON;
+  }
+
+  @Override
+  @NotNull
+  public String getId() {
+    return "BlazeAndroidTestRunConfigurationType";
+  }
+
+  @Override
+  public BlazeAndroidTestRunConfigurationFactory[] getConfigurationFactories() {
+    return new BlazeAndroidTestRunConfigurationFactory[]{factory};
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
new file mode 100644
index 0000000..034c7b6
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/BlazeAndroidTestRunContext.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.*;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.editor.AndroidDebuggerState;
+import com.android.tools.idea.run.tasks.DebugConnectorTask;
+import com.android.tools.idea.run.tasks.DeployApkTask;
+import com.android.tools.idea.run.tasks.LaunchTask;
+import com.android.tools.idea.run.tasks.LaunchTasksProvider;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfigurationCommonState;
+import com.google.idea.blaze.android.run.deployinfo.BlazeApkProvider;
+import com.google.idea.blaze.android.run.runner.*;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Run context for android_test.
+ */
+class BlazeAndroidTestRunContext implements BlazeAndroidRunContext {
+  private final Project project;
+  private final AndroidFacet facet;
+  private final RunConfiguration runConfiguration;
+  private final ExecutionEnvironment env;
+  private final BlazeAndroidRunConfigurationCommonState commonState;
+  private final BlazeAndroidTestRunConfigurationState configState;
+  private final ImmutableList<String> buildFlags;
+  private final List<Runnable> launchTaskCompleteListeners = Lists.newArrayList();
+  private final ConsoleProvider consoleProvider;
+  private final BlazeApkBuildStepNormalBuild buildStep;
+  private final ApplicationIdProvider applicationIdProvider;
+  private final ApkProvider apkProvider;
+
+  public BlazeAndroidTestRunContext(Project project,
+                                    AndroidFacet facet,
+                                    RunConfiguration runConfiguration,
+                                    ExecutionEnvironment env,
+                                    BlazeAndroidRunConfigurationCommonState commonState,
+                                    BlazeAndroidTestRunConfigurationState configState,
+                                    ImmutableList<String> buildFlags) {
+    this.project = project;
+    this.facet = facet;
+    this.runConfiguration = runConfiguration;
+    this.env = env;
+    this.commonState = commonState;
+    this.configState = configState;
+    this.buildFlags = buildFlags;
+    this.consoleProvider = new AndroidTestConsoleProvider(project, runConfiguration, configState);
+    this.buildStep = new BlazeApkBuildStepNormalBuild(project, commonState, buildFlags);
+    this.applicationIdProvider = new BlazeAndroidTestApplicationIdProvider(project, buildStep.getDeployInfo());
+    this.apkProvider = new BlazeApkProvider(project, buildStep.getDeployInfo());
+  }
+
+  @Override
+  public void augmentEnvironment(ExecutionEnvironment env) {
+  }
+
+  @Override
+  public BlazeAndroidDeviceSelector getDeviceSelector() {
+    return new BlazeAndroidDeviceSelector.NormalDeviceSelector();
+  }
+
+  @Override
+  public void augmentLaunchOptions(LaunchOptions.Builder options) {
+    options.setDeploy(!configState.isRunThroughBlaze());
+  }
+
+  @Override
+  public ConsoleProvider getConsoleProvider() {
+    return consoleProvider;
+  }
+
+  @Override
+  public ApplicationIdProvider getApplicationIdProvider() throws ExecutionException {
+    return applicationIdProvider;
+  }
+
+  @Nullable
+  @Override
+  public BlazeApkBuildStep getBuildStep() {
+    return buildStep;
+  }
+
+  @Override
+  public LaunchTasksProvider getLaunchTasksProvider(
+    LaunchOptions launchOptions,
+    BlazeAndroidRunConfigurationDebuggerManager debuggerManager) throws ExecutionException {
+    return new BlazeAndroidLaunchTasksProvider(project, this, applicationIdProvider, launchOptions, debuggerManager);
+  }
+
+  @Override
+  public ImmutableList<LaunchTask> getDeployTasks(IDevice device, LaunchOptions launchOptions) throws ExecutionException {
+    Collection<ApkInfo> apks;
+    try {
+      apks = apkProvider.getApks(device);
+    }
+    catch (ApkProvisionException e) {
+      throw new ExecutionException(e);
+    }
+    return ImmutableList.of(new DeployApkTask(project, launchOptions, apks));
+  }
+
+  @Nullable
+  @Override
+  public LaunchTask getApplicationLaunchTask(LaunchOptions launchOptions,
+                                             AndroidDebugger androidDebugger,
+                                             AndroidDebuggerState androidDebuggerState,
+                                             ProcessHandlerLaunchStatus processHandlerLaunchStatus) throws ExecutionException {
+    if (configState.isRunThroughBlaze()) {
+      return new BlazeAndroidTestLaunchTask(
+        project,
+        commonState.getTarget(),
+        buildFlags,
+        new BlazeAndroidTestFilter(configState.TESTING_TYPE,
+                                   configState.CLASS_NAME,
+                                   configState.METHOD_NAME,
+                                   configState.PACKAGE_NAME),
+        this,
+        launchOptions.isDebug()
+      );
+    }
+    return StockAndroidTestLaunchTask.getStockTestLaunchTask(
+      configState,
+      applicationIdProvider,
+      launchOptions.isDebug(),
+      facet,
+      processHandlerLaunchStatus
+    );
+  }
+
+  @Override
+  public DebugConnectorTask getDebuggerTask(LaunchOptions launchOptions,
+                                            AndroidDebugger androidDebugger,
+                                            AndroidDebuggerState androidDebuggerState,
+                                            @NotNull Set<String> packageIds) throws ExecutionException {
+    if (configState.isRunThroughBlaze()) {
+      return new ConnectBlazeTestDebuggerTask(env.getProject(), androidDebugger, packageIds, applicationIdProvider, this);
+    }
+    //noinspection unchecked
+    return androidDebugger.getConnectDebuggerTask(env, null, packageIds, facet, androidDebuggerState, runConfiguration.getType().getId());
+  }
+
+  void onLaunchTaskComplete() {
+    for (Runnable runnable : launchTaskCompleteListeners) {
+      runnable.run();
+    }
+  }
+
+  void addLaunchTaskCompleteListener(Runnable runnable) {
+    launchTaskCompleteListeners.add(runnable);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/ConnectBlazeTestDebuggerTask.java b/aswb/src/com/google/idea/blaze/android/run/test/ConnectBlazeTestDebuggerTask.java
new file mode 100644
index 0000000..d15ecb6
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/ConnectBlazeTestDebuggerTask.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.android.ddmlib.AndroidDebugBridge;
+import com.android.ddmlib.Client;
+import com.android.ddmlib.ClientData;
+import com.android.ddmlib.IDevice;
+import com.android.tools.idea.run.*;
+import com.android.tools.idea.run.editor.AndroidDebugger;
+import com.android.tools.idea.run.tasks.ConnectDebuggerTask;
+import com.android.tools.idea.run.tasks.ConnectJavaDebuggerTask;
+import com.android.tools.idea.run.util.ProcessHandlerLaunchStatus;
+import com.intellij.debugger.engine.RemoteDebugProcessHandler;
+import com.intellij.debugger.ui.DebuggerPanelsManager;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.RemoteConnection;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.runners.ExecutionEnvironmentBuilder;
+import com.intellij.execution.ui.RunContentDescriptor;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * Connects the blaze debugger during execution.
+ */
+class ConnectBlazeTestDebuggerTask extends ConnectDebuggerTask {
+  private static final Logger LOG = Logger.getInstance(ConnectBlazeTestDebuggerTask.class);
+
+  private final Project project;
+  private final ApplicationIdProvider applicationIdProvider;
+  private final BlazeAndroidTestRunContext runContext;
+
+  public ConnectBlazeTestDebuggerTask(
+    Project project,
+    AndroidDebugger debugger,
+    Set<String> applicationIds,
+    ApplicationIdProvider applicationIdProvider,
+    BlazeAndroidTestRunContext runContext) {
+    super(applicationIds, debugger, project);
+    this.project = project;
+    this.applicationIdProvider = applicationIdProvider;
+    this.runContext = runContext;
+  }
+
+  @Nullable
+  @Override
+  public ProcessHandler perform(@NotNull LaunchInfo launchInfo,
+                                @NotNull IDevice device,
+                                @NotNull ProcessHandlerLaunchStatus state,
+                                @NotNull ProcessHandlerConsolePrinter printer) {
+    try {
+      String packageName = applicationIdProvider.getPackageName();
+      setUpForReattachingDebugger(packageName, launchInfo, state, printer);
+    }
+    catch (ApkProvisionException e) {
+      LOG.error(e);
+    }
+
+    // The return value for this task is not used
+    return null;
+  }
+
+  /**
+   * Wires up listeners to automatically reconnect the debugger for each test method.
+   * When you `blaze test` an android_test in debug mode, it kills the instrumentation process
+   * between each test method, disconnecting the debugger. We listen for the start of a new
+   * method waiting for a debugger, and reconnect.
+   * TODO: Support stopping Blaze from the UI. This is hard because we have no way to distinguish
+   *   process handler termination/debug session ending initiated by the user.
+   */
+  private void setUpForReattachingDebugger(
+    String targetPackage,
+    LaunchInfo launchInfo,
+    ProcessHandlerLaunchStatus launchStatus,
+    ProcessHandlerConsolePrinter printer
+  ) {
+    final AndroidDebugBridge.IClientChangeListener reattachingListener =
+      new AndroidDebugBridge.IClientChangeListener() {
+        // The target application can either
+        // 1. Match our target name, and become available for debugging.
+        // 2. Be available for debugging, and suddenly have its name changed to match.
+        static final int CHANGE_MASK = Client.CHANGE_DEBUGGER_STATUS | Client.CHANGE_NAME;
+
+        @Override
+        public void clientChanged(@NotNull Client client, int changeMask) {
+          ClientData data = client.getClientData();
+          String clientDescription = data.getClientDescription();
+          if (clientDescription != null && clientDescription.equals(targetPackage)
+              && (changeMask & CHANGE_MASK) != 0
+              && data.getDebuggerConnectionStatus().equals(ClientData.DebuggerStatus.WAITING)) {
+            reattachDebugger(launchInfo, client, launchStatus, printer);
+          }
+        }
+      };
+
+    AndroidDebugBridge.addClientChangeListener(reattachingListener);
+    runContext.addLaunchTaskCompleteListener(() -> {
+      AndroidDebugBridge.removeClientChangeListener(reattachingListener);
+      launchStatus.terminateLaunch("Test run completed.\n");
+    });
+  }
+
+  private void reattachDebugger(
+    LaunchInfo launchInfo,
+    final Client client,
+    ProcessHandlerLaunchStatus launchStatus,
+    ProcessHandlerConsolePrinter printer
+  ) {
+    ApplicationManager.getApplication().invokeLater(() -> launchDebugger(launchInfo, client, launchStatus, printer));
+  }
+
+  /**
+   * Nearly a clone of {@link ConnectJavaDebuggerTask#launchDebugger}. There are a few changes to account for null variables that could
+   * occur in our implementation.
+   */
+  @Override
+  public ProcessHandler launchDebugger(@NotNull LaunchInfo currentLaunchInfo,
+                                       @NotNull Client client,
+                                       @NotNull ProcessHandlerLaunchStatus launchStatus,
+                                       @NotNull ProcessHandlerConsolePrinter printer) {
+    String debugPort = Integer.toString(client.getDebuggerListenPort());
+    int pid = client.getClientData().getPid();
+    Logger.getInstance(ConnectJavaDebuggerTask.class)
+      .info(String.format(Locale.US, "Attempting to connect debugger to port %1$s [client %2$d]", debugPort, pid));
+
+    // create a new process handler
+    RemoteConnection connection = new RemoteConnection(true, "localhost", debugPort, false);
+    RemoteDebugProcessHandler debugProcessHandler = new RemoteDebugProcessHandler(project);
+
+    // switch the launch status and console printers to point to the new process handler
+    // this is required, esp. for AndroidTestListener which holds a reference to the launch status and printers, and those should
+    // be updated to point to the new process handlers, otherwise test results will not be forwarded appropriately
+    launchStatus.setProcessHandler(debugProcessHandler);
+    printer.setProcessHandler(debugProcessHandler);
+
+    // detach old process handler
+    RunContentDescriptor descriptor = currentLaunchInfo.env.getContentToReuse();
+    assert descriptor != null;
+
+    final ProcessHandler processHandler = descriptor.getProcessHandler();
+
+    // detach after the launch status has been updated to point to the new process handler
+    if (processHandler != null) {
+      processHandler.detachProcess();
+    }
+
+    AndroidDebugState debugState = new AndroidDebugState(project, debugProcessHandler, connection, currentLaunchInfo.consoleProvider);
+
+    RunContentDescriptor debugDescriptor;
+    try {
+      // @formatter:off
+      ExecutionEnvironment debugEnv = new ExecutionEnvironmentBuilder(currentLaunchInfo.env)
+        .executor(currentLaunchInfo.executor)
+        .runner(currentLaunchInfo.runner)
+        .contentToReuse(processHandler == null ? null : descriptor)
+        .build();
+      debugDescriptor = DebuggerPanelsManager.getInstance(project).attachVirtualMachine(debugEnv, debugState, connection, false);
+      // @formatter:on
+    }
+    catch (ExecutionException e) {
+      printer.stderr("ExecutionException: " + e.getMessage() + '.');
+      return null;
+    }
+
+    // Based on the above try block, we shouldn't get here unless we have assigned to debugDescriptor
+    assert debugDescriptor != null;
+
+    // re-run the collected text from the old process handler to the new
+    // TODO: is there a race between messages received once the debugger has been connected, and these messages that are printed out?
+    if (processHandler != null) {
+      final AndroidProcessText oldText = AndroidProcessText.get(processHandler);
+      if (oldText != null) {
+        oldText.printTo(debugProcessHandler);
+      }
+    }
+
+    RunProfile runProfile = currentLaunchInfo.env.getRunProfile();
+    int uniqueId = runProfile instanceof AndroidRunConfigurationBase ? ((AndroidRunConfigurationBase)runProfile).getUniqueID() : -1;
+    AndroidSessionInfo value =
+      new AndroidSessionInfo(debugProcessHandler, debugDescriptor, uniqueId, currentLaunchInfo.executor.getId(), false);
+    debugProcessHandler.putUserData(AndroidSessionInfo.KEY, value);
+    debugProcessHandler.putUserData(AndroidSessionInfo.ANDROID_DEBUG_CLIENT, client);
+    debugProcessHandler.putUserData(AndroidSessionInfo.ANDROID_DEVICE_API_LEVEL, client.getDevice().getVersion());
+
+    return debugProcessHandler;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/InstrumentationRunnerProvider.java b/aswb/src/com/google/idea/blaze/android/run/test/InstrumentationRunnerProvider.java
new file mode 100644
index 0000000..3d7b435
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/InstrumentationRunnerProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides a default instrumentation test runner class for android test configurations.
+ */
+public interface InstrumentationRunnerProvider {
+
+  ExtensionPointName<InstrumentationRunnerProvider> EP_NAME =
+    ExtensionPointName.create("com.google.idea.blaze.android.InstrumentationRunnerProvider");
+
+  @Nullable
+  static String getDefaultInstrumentationRunnerClass() {
+    for (InstrumentationRunnerProvider provider : EP_NAME.getExtensions()) {
+      String path = provider.getInstrumentationRunnerClass();
+      if (path != null) {
+        return path;
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  String getInstrumentationRunnerClass();
+
+}
diff --git a/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java b/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java
new file mode 100644
index 0000000..10c3fd1
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/run/test/StockAndroidTestLaunchTask.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.run.test;
+
+import com.android.builder.model.Variant;
+import com.android.ddmlib.IDevice;
+import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
+import com.android.tools.idea.gradle.AndroidGradleModel;
+import com.android.tools.idea.run.ApkProvisionException;
+import com.android.tools.idea.run.ApplicationIdProvider;
+import com.android.tools.idea.run.ConsolePrinter;
+import com.android.tools.idea.run.tasks.LaunchTask;
+import com.android.tools.idea.run.testing.AndroidTestListener;
+import com.android.tools.idea.run.util.LaunchStatus;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.Computable;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiClass;
+import org.jetbrains.android.dom.manifest.Instrumentation;
+import org.jetbrains.android.dom.manifest.Manifest;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+final class StockAndroidTestLaunchTask implements LaunchTask {
+  private static final Logger LOG = Logger.getInstance(StockAndroidTestLaunchTask.class);
+
+  @NotNull private final BlazeAndroidTestRunConfigurationState configState;
+  @Nullable private final String instrumentationTestRunner;
+  @NotNull private final String testApplicationId;
+  private final boolean waitForDebugger;
+
+  private StockAndroidTestLaunchTask(@NotNull BlazeAndroidTestRunConfigurationState configState, @Nullable String runner, @NotNull String testPackage, boolean waitForDebugger) {
+    this.configState = configState;
+    this.instrumentationTestRunner = runner;
+    this.waitForDebugger = waitForDebugger;
+    this.testApplicationId = testPackage;
+  }
+
+  public static LaunchTask getStockTestLaunchTask(
+    @NotNull BlazeAndroidTestRunConfigurationState configState,
+    @NotNull ApplicationIdProvider applicationIdProvider,
+    boolean waitForDebugger,
+    @NotNull AndroidFacet facet,
+    @NotNull LaunchStatus launchStatus
+  ) {
+    String runner = StringUtil.isEmpty(configState.INSTRUMENTATION_RUNNER_CLASS)
+                    ? findInstrumentationRunner(facet)
+                    : configState.INSTRUMENTATION_RUNNER_CLASS;
+    String testPackage;
+    try {
+      testPackage = applicationIdProvider.getTestPackageName();
+      if (testPackage == null) {
+        launchStatus.terminateLaunch("Unable to determine test package name");
+        return null;
+      }
+    }
+    catch (ApkProvisionException e) {
+      launchStatus.terminateLaunch("Unable to determine test package name");
+      return null;
+    }
+
+    return new StockAndroidTestLaunchTask(configState, runner, testPackage, waitForDebugger);
+  }
+
+  @Nullable
+  private static String findInstrumentationRunner(@NotNull AndroidFacet facet) {
+    String runner = getRunnerFromManifest(facet);
+
+    // TODO: Resolve direct AndroidGradleModel dep (b/22596984)
+    AndroidGradleModel androidModel = AndroidGradleModel.get(facet);
+    if (runner == null && androidModel != null) {
+      Variant selectedVariant = androidModel.getSelectedVariant();
+      String testRunner = selectedVariant.getMergedFlavor().getTestInstrumentationRunner();
+      if (testRunner != null) {
+        runner = testRunner;
+      }
+    }
+
+    return runner;
+  }
+
+  @Nullable
+  private static String getRunnerFromManifest(@NotNull final AndroidFacet facet) {
+    if (!ApplicationManager.getApplication().isReadAccessAllowed()) {
+      return ApplicationManager.getApplication().runReadAction(new Computable<String>() {
+        @Override
+        public String compute() {
+          return getRunnerFromManifest(facet);
+        }
+      });
+    }
+
+    Manifest manifest = facet.getManifest();
+    if (manifest != null) {
+      for (Instrumentation instrumentation : manifest.getInstrumentations()) {
+        if (instrumentation != null) {
+          PsiClass instrumentationClass = instrumentation.getInstrumentationClass().getValue();
+          if (instrumentationClass != null) {
+            return instrumentationClass.getQualifiedName();
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+
+  @NotNull
+  @Override
+  public String getDescription() {
+    return "Launching instrumentation runner";
+  }
+
+  @Override
+  public int getDuration() {
+    return 2;
+  }
+
+  @Override
+  public boolean perform(@NotNull IDevice device, @NotNull final LaunchStatus launchStatus, @NotNull final ConsolePrinter printer) {
+    printer.stdout("Running tests\n");
+
+    final RemoteAndroidTestRunner runner = new RemoteAndroidTestRunner(testApplicationId, instrumentationTestRunner, device);
+    switch (configState.TESTING_TYPE) {
+      case BlazeAndroidTestRunConfigurationState.TEST_ALL_IN_PACKAGE:
+        runner.setTestPackageName(configState.PACKAGE_NAME);
+        break;
+      case BlazeAndroidTestRunConfigurationState.TEST_CLASS:
+        runner.setClassName(configState.CLASS_NAME);
+        break;
+      case BlazeAndroidTestRunConfigurationState.TEST_METHOD:
+        runner.setMethodName(configState.CLASS_NAME, configState.METHOD_NAME);
+        break;
+    }
+    runner.setDebug(waitForDebugger);
+    runner.setRunOptions(configState.EXTRA_OPTIONS);
+
+    printer.stdout("$ adb shell " + runner.getAmInstrumentCommand());
+
+    // run in a separate thread as this will block until the tests complete
+    ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          runner.run(new AndroidTestListener(launchStatus, printer));
+        }
+        catch (Exception e) {
+          LOG.info(e);
+          printer.stderr("Error: Unexpected exception while running tests: " + e);
+        }
+      }
+    });
+
+    return true;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/AndroidSdkListener.java b/aswb/src/com/google/idea/blaze/android/sdk/AndroidSdkListener.java
new file mode 100644
index 0000000..b58f3f5
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sdk/AndroidSdkListener.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sdk;
+
+import com.android.tools.idea.sdk.IdeSdks;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatus;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * Listens for android SDK changes, and queues up a blaze sync
+ */
+public class AndroidSdkListener implements IdeSdks.AndroidSdkEventListener {
+
+  @Override
+  public void afterSdkPathChange(@NotNull File sdkPath, @NotNull Project project) {
+    if (Blaze.isBlazeProject(project)) {
+      BlazeSyncStatus.getInstance(project).setDirty();
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sdk/SdkUtil.java b/aswb/src/com/google/idea/blaze/android/sdk/SdkUtil.java
new file mode 100644
index 0000000..2c56c29
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sdk/SdkUtil.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.android.sdk;
+
+import com.google.idea.blaze.android.sync.AndroidSdkPlatformSyncer;
+import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import org.jetbrains.android.sdk.AndroidPlatform;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * SDK utilities.
+ */
+public class SdkUtil {
+  @Nullable
+  public static AndroidPlatform getAndroidPlatform(@NotNull Project project) {
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
+    AndroidSdkPlatform androidSdkPlatform = AndroidSdkPlatformSyncer.getAndroidSdkPlatform(blazeProjectData);
+    if (androidSdkPlatform == null) {
+      return null;
+    }
+    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
+    if (sdk == null) {
+      return null;
+    }
+    return AndroidPlatform.getInstance(sdk);
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java b/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java
new file mode 100644
index 0000000..53992b0
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/settings/AswbGlobalSettings.java
@@ -0,0 +1,55 @@
+/*
+ * 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.*;
+import com.intellij.util.xmlb.XmlSerializerUtil;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Stores aswb global settings.
+ */
+@State(
+  name = "AswbGlobalSettings",
+  storages = @Storage(file = StoragePathMacros.APP_CONFIG + "/aswb.global.xml")
+)
+public class AswbGlobalSettings implements PersistentStateComponent<AswbGlobalSettings> {
+
+  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);
+  }
+
+  public void setLocalSdkLocation(String localSdkLocation) {
+    this.localSdkLocation = localSdkLocation;
+  }
+
+  public String getLocalSdkLocation() {
+    return localSdkLocation;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/AndroidSdkPlatformSyncer.java b/aswb/src/com/google/idea/blaze/android/sync/AndroidSdkPlatformSyncer.java
new file mode 100644
index 0000000..2ee6d56
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/AndroidSdkPlatformSyncer.java
@@ -0,0 +1,120 @@
+/*
+ * 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.common.collect.Iterables;
+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.android.sync.model.BlazeAndroidSyncData;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.Sdk;
+import org.jetbrains.android.sdk.AndroidPlatform;
+import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * Calculates AndroidSdkPlatform.
+ */
+public class AndroidSdkPlatformSyncer {
+  @Nullable
+  static AndroidSdkPlatform getAndroidSdkPlatform(
+    Project project,
+    BlazeContext context,
+    File androidPlatformDirectory) {
+
+    String androidSdk = null;
+
+    String localSdkLocation = AswbGlobalSettings.getInstance().getLocalSdkLocation();
+    if (localSdkLocation == null) {
+      IssueOutput
+        .error("Error: No android_sdk synced yet. Please sync SDK following go/aswb-sdk.")
+        .submit(context);
+    }
+
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet != null) {
+      Collection<ScalarSection<String>> androidSdkPlatformSections =
+        projectViewSet.getSections(AndroidSdkPlatformSection.KEY);
+      if (!androidSdkPlatformSections.isEmpty()) {
+        ScalarSection<String> androidSdkPlatformSection = Iterables.getLast(androidSdkPlatformSections);
+        androidSdk = BlazeAndroidSdk.getAndroidSdkLevelFromLocalChannel(
+          localSdkLocation,
+          androidSdkPlatformSection.getValue());
+
+        if (androidSdk == null) {
+          IssueOutput
+            .error("No such android_sdk_platform: " + androidSdkPlatformSection.getValue())
+            .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
+            .submit(context);
+        }
+      }
+    }
+
+    if (androidSdk == null) {
+      androidSdk = BlazeAndroidSdk.getAndroidSdkLevelFromBlazeRc(androidPlatformDirectory);
+    }
+
+    if (androidSdk == null) {
+      IssueOutput
+        .error("Can't determine your SDK. Please sync your SDK by following go/aswb-sdk and try again.")
+        .submit(context);
+      return null;
+    }
+
+    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdk);
+    if (sdk == null) {
+      IssueOutput
+        .error("Can't find a matching SDK. Please sync your SDK by following go/aswb-sdk and try again.")
+        .submit(context);
+      return null;
+    }
+
+    int androidSdkApiLevel = getAndroidSdkApiLevel(androidSdk);
+    return new AndroidSdkPlatform(androidSdk, androidSdkApiLevel);
+  }
+
+  @Nullable
+  static public AndroidSdkPlatform getAndroidSdkPlatform(BlazeProjectData blazeProjectData) {
+    BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
+    return syncData != null ? syncData.androidSdkPlatform : null;
+  }
+
+  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/BlazeAndroidJavaSyncAugmenter.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidJavaSyncAugmenter.java
new file mode 100644
index 0000000..ea02f53
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidJavaSyncAugmenter.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.android.sync;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.section.Glob;
+import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+
+import java.util.Collection;
+
+/**
+ * Augments the java sync process with Android support.
+ */
+public class BlazeAndroidJavaSyncAugmenter implements BlazeJavaSyncAugmenter {
+  private static final BoolExperiment EXCLUDE_ANDROID_BLAZE_JAR = new BoolExperiment("exclude.android.blaze.jar", true);
+
+  @Override
+  public void addLibraryFilter(Glob.GlobSet excludedLibraries) {
+    if (EXCLUDE_ANDROID_BLAZE_JAR.getValue()) {
+      excludedLibraries.add(new Glob("*/android_blaze.jar")); // This is supplied via the SDK
+    }
+  }
+
+  @Override
+  public Collection<BlazeLibrary> getAdditionalLibraries(BlazeProjectData blazeProjectData) {
+    BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
+    if (syncData == null) {
+      return ImmutableList.of();
+    }
+    return syncData.importResult.libraries;
+  }
+
+  @Override
+  public Collection<String> getExternallyAddedLibraries(BlazeProjectData blazeProjectData) {
+    return ImmutableList.of();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSdk.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSdk.java
new file mode 100644
index 0000000..826e1c4
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSdk.java
@@ -0,0 +1,120 @@
+/*
+ * 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.android.SdkConstants;
+import com.android.sdklib.AndroidTargetHash;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.AndroidVersionHelper;
+import com.android.tools.idea.sdk.IdeSdks;
+import com.intellij.openapi.application.ApplicationManager;
+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.
+ */
+public 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);
+  }
+
+  /**
+   * The user's blazerc is read to discover the --android_sdk configuration. The resulting
+   * label can be an indirection (eg. "latest" or "prerelease"), so blaze query is used to
+   * query for "android_blaze.jar", which marks the actual android sdk directory.
+   * <p/>
+   * <p>This directory should have a source.properties file. This is read to discover the
+   * platform API level (eg. "21"). This is hashed according to ADT standards to return
+   * a unique SDK key, eg "android-21".
+   */
+  @Nullable
+  public static String getAndroidSdkLevelFromBlazeRc(
+    File androidPlatformDir) {
+    File sourcePropertiesFile = new File(androidPlatformDir, 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/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
new file mode 100644
index 0000000..6fa349a
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncListener.java
@@ -0,0 +1,48 @@
+/*
+ * 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.android.tools.idea.res.ResourceFolderRegistry;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.intellij.openapi.project.DumbService;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Android-specific hooks to run after a blaze sync.
+ */
+public class BlazeAndroidSyncListener implements SyncListener {
+  @Override
+  public void onSyncStart(Project project) {
+  }
+
+  @Override
+  public void onSyncComplete(Project project,
+                             BlazeImportSettings importSettings,
+                             ProjectViewSet projectViewSet,
+                             BlazeProjectData blazeProjectData) {
+  }
+
+  @Override
+  public void afterSync(Project project, boolean successful) {
+    if (successful) {
+      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
new file mode 100644
index 0000000..2760980
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/BlazeAndroidSyncPlugin.java
@@ -0,0 +1,277 @@
+/*
+ * 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.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+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.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.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+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.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.java.projectview.JavaLanguageLevelSection;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+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.LanguageLevelProjectExtension;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
+import com.intellij.pom.java.LanguageLevel;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.android.sdk.AndroidSdkUtils;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * ASwB sync plugin.
+ */
+public class BlazeAndroidSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  @Nullable
+  @Override
+  public WorkspaceType getDefaultWorkspaceType() {
+    return WorkspaceType.ANDROID;
+  }
+
+  @Nullable
+  @Override
+  public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.ANDROID || workspaceType == WorkspaceType.ANDROID_NDK) {
+      return StdModuleTypes.JAVA;
+    }
+    return null;
+  }
+
+  @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();
+    }
+  }
+
+  @Override
+  public void updateSyncState(Project project,
+                              BlazeContext context,
+                              WorkspaceRoot workspaceRoot,
+                              ProjectViewSet projectViewSet,
+                              WorkspaceLanguageSettings workspaceLanguageSettings,
+                              BlazeRoots blazeRoots,
+                              @Nullable WorkingSet workingSet,
+                              WorkspacePathResolver workspacePathResolver,
+                              ImmutableMap<Label, RuleIdeInfo> ruleMap,
+                              @Deprecated @Nullable File androidPlatformDirectory,
+                              SyncState.Builder syncStateBuilder,
+                              @Nullable SyncState previousSyncState) {
+    if (!isAndroidWorkspace(workspaceLanguageSettings)) {
+      return;
+    }
+
+    AndroidSdkPlatform androidSdkPlatform = AndroidSdkPlatformSyncer.getAndroidSdkPlatform(project, context, androidPlatformDirectory);
+    BlazeAndroidWorkspaceImporter workspaceImporter = new BlazeAndroidWorkspaceImporter(
+      project,
+      context,
+      workspaceRoot,
+      projectViewSet,
+      ruleMap
+    );
+    BlazeAndroidImportResult importResult = Scope.push(context, (childContext) -> {
+      childContext.push(new TimingScope("AndroidWorkspaceImporter"));
+      return workspaceImporter.importWorkspace();
+    });
+    BlazeAndroidSyncData syncData = new BlazeAndroidSyncData(importResult, androidSdkPlatform);
+    syncStateBuilder.put(BlazeAndroidSyncData.class, syncData);
+  }
+
+  @Override
+  public void updateSdk(Project project,
+                        BlazeContext context,
+                        ProjectViewSet projectViewSet,
+                        BlazeProjectData blazeProjectData) {
+    if (!isAndroidWorkspace(blazeProjectData.workspaceLanguageSettings)) {
+      return;
+    }
+    BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
+    if (syncData == null) {
+      return;
+    }
+    AndroidSdkPlatform androidSdkPlatform = syncData.androidSdkPlatform;
+    if (androidSdkPlatform == null) {
+      return;
+    }
+    Sdk sdk = AndroidSdkUtils.findSuitableAndroidSdk(androidSdkPlatform.androidSdk);
+    if (sdk == null) {
+      IssueOutput
+        .error(String.format("Android platform '%s' not found.", androidSdkPlatform.androidSdk))
+        .submit(context);
+      return;
+    }
+
+    LanguageLevel javaLanguageLevel = JavaLanguageLevelSection.getLanguageLevel(projectViewSet, LanguageLevel.JDK_1_7);
+    setProjectSdkAndLanguageLevel(project, sdk, javaLanguageLevel);
+  }
+
+  @Override
+  public void updateProjectStructure(Project project,
+                                     BlazeContext context,
+                                     WorkspaceRoot workspaceRoot,
+                                     ProjectViewSet projectViewSet,
+                                     BlazeProjectData blazeProjectData,
+                                     @Nullable BlazeProjectData oldBlazeProjectData,
+                                     ModuleEditor moduleEditor,
+                                     Module workspaceModule,
+                                     ModifiableRootModel workspaceModifiableModel) {
+    BlazeAndroidProjectStructureSyncer.updateProjectStructure(
+      project,
+      context,
+      workspaceRoot,
+      projectViewSet,
+      blazeProjectData,
+      moduleEditor,
+      workspaceModule,
+      workspaceModifiableModel,
+      isAndroidWorkspace(blazeProjectData.workspaceLanguageSettings)
+    );
+  }
+
+  @Override
+  public boolean validate(Project project,
+                          BlazeContext context,
+                          BlazeProjectData blazeProjectData) {
+    if (!isAndroidWorkspace(blazeProjectData.workspaceLanguageSettings)) {
+      return true;
+    }
+    
+    boolean valid = true;
+    for (Module module : ModuleManager.getInstance(project).getModules()) {
+      AndroidFacet facet = AndroidFacet.getInstance(module);
+      if (facet != null && facet.requiresAndroidModel() && facet.getAndroidModel() == null) {
+        IssueOutput.error("Android model missing for module: " + module.getName())
+          .submit(context);
+        valid = false;
+      }
+    }
+    return valid;
+  }
+
+  @Override
+  public boolean validateProjectView(BlazeContext context,
+                                     ProjectViewSet projectViewSet,
+                                     WorkspaceLanguageSettings workspaceLanguageSettings) {
+    if (!isAndroidWorkspace(workspaceLanguageSettings)) {
+      return true;
+    }
+
+    if (workspaceLanguageSettings.isWorkspaceType(WorkspaceType.ANDROID_NDK) && !NdkSupport.NDK_SUPPORT.getValue()) {
+      IssueOutput
+        .error("Android NDK is not supported yet.")
+        .submit(context);
+      return false;
+    }
+
+    Collection<ScalarSection<String>> androidSdkPlatformSections =
+      projectViewSet.getSections(AndroidSdkPlatformSection.KEY);
+    if (androidSdkPlatformSections.isEmpty()) {
+      String error = Joiner.on('\n').join(
+        "You should specify the android SDK platform in your '.asproject' file.",
+        "To set this, first ensure your SDK is up-to-date (go/aswb-sdk)",
+        "Then add an 'android_sdk_platform' line to your .asproject file,",
+        "e.g. 'android_sdk_platform: \"android-N\"', where 'android-N' is a",
+        "platform directory name in your local SDK directory.",
+        "",
+        "NOTE: This will become an error starting from the next release."
+      );
+      IssueOutput
+        .warn(error)
+        .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
+        .submit(context);
+    }
+    return true;
+  }
+
+  private static void setProjectSdkAndLanguageLevel(
+    final Project project,
+    final Sdk sdk,
+    final LanguageLevel javaLanguageLevel) {
+    UIUtil.invokeAndWaitIfNeeded((Runnable)() -> ApplicationManager.getApplication().runWriteAction(() -> {
+      ProjectRootManagerEx rootManager = ProjectRootManagerEx.getInstanceEx(project);
+      rootManager.setProjectSdk(sdk);
+      LanguageLevelProjectExtension ext = LanguageLevelProjectExtension.getInstance(project);
+      ext.setLanguageLevel(javaLanguageLevel);
+    }));
+  }
+
+  @Override
+  public Collection<SectionParser> getSections() {
+    return ImmutableList.of(
+      AndroidSdkPlatformSection.PARSER
+    );
+  }
+
+  @Override
+  public boolean requiresAndroidSdk(WorkspaceLanguageSettings workspaceLanguageSettings) {
+    return isAndroidWorkspace(workspaceLanguageSettings);
+  }
+
+  private static boolean isAndroidWorkspace(WorkspaceLanguageSettings workspaceLanguageSettings) {
+    return workspaceLanguageSettings.isWorkspaceType(WorkspaceType.ANDROID, WorkspaceType.ANDROID_NDK);
+  }
+
+  @Override
+  public Set<String> prefetchSrcFileExtensions() {
+    return ImmutableSet.of("xml");
+  }
+}
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
new file mode 100644
index 0000000..c67da3c
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporter.java
@@ -0,0 +1,250 @@
+/*
+ * 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;
+
+import com.google.common.collect.*;
+import com.google.idea.blaze.android.sync.importer.aggregators.TransitiveResourceMap;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.PerformanceWarning;
+import com.google.idea.blaze.base.sync.projectview.ProjectViewRuleImportFilter;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Builds a BlazeWorkspace.
+ */
+public final class BlazeAndroidWorkspaceImporter {
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidWorkspaceImporter.class);
+  private static final BoolExperiment DISCARD_ANDROID_BINARY_RESOURCE_JAR = new BoolExperiment("discard.android.binary.resource.jar", true);
+
+  private final Project project;
+  private final BlazeContext context;
+  private final WorkspaceRoot workspaceRoot;
+  private final ImmutableMap<Label, RuleIdeInfo> ruleMap;
+  private final ProjectViewRuleImportFilter importFilter;
+  private final boolean discardAndroidBinaryResourceJar;
+
+  public BlazeAndroidWorkspaceImporter(
+    Project project,
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet,
+    ImmutableMap<Label, RuleIdeInfo> ruleMap) {
+    this.project = project;
+    this.context = context;
+    this.workspaceRoot = workspaceRoot;
+    this.ruleMap = ruleMap;
+    this.importFilter = new ProjectViewRuleImportFilter(project, workspaceRoot, projectViewSet);
+    this.discardAndroidBinaryResourceJar = DISCARD_ANDROID_BINARY_RESOURCE_JAR.getValue();
+  }
+
+  public BlazeAndroidImportResult importWorkspace() {
+    List<RuleIdeInfo> rules = ruleMap.values()
+      .stream()
+      .filter(rule -> rule.kind.getLanguageClass() == LanguageClass.ANDROID)
+      .filter(importFilter::isSourceRule)
+      .filter(rule -> !importFilter.excludeTarget(rule))
+      .collect(Collectors.toList());
+
+    TransitiveResourceMap transitiveResourceMap = new TransitiveResourceMap(ruleMap);
+
+    WorkspaceBuilder workspaceBuilder = new WorkspaceBuilder();
+
+    for (RuleIdeInfo rule : rules) {
+      addRule(
+        workspaceBuilder,
+        transitiveResourceMap,
+        rule
+      );
+    }
+
+    ImmutableList<AndroidResourceModule> androidResourceModules = buildAndroidResourceModules(workspaceBuilder);
+    BlazeLibrary resourceLibrary = createResourceLibrary(androidResourceModules);
+    if (resourceLibrary != null) {
+      workspaceBuilder.libraries.add(resourceLibrary);
+    }
+
+    return new BlazeAndroidImportResult(
+      androidResourceModules,
+      workspaceBuilder.libraries.build()
+    );
+  }
+
+  private void addRule(
+    WorkspaceBuilder workspaceBuilder,
+    TransitiveResourceMap transitiveResourceMap,
+    RuleIdeInfo rule) {
+
+    AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
+    if (androidRuleIdeInfo != null) {
+      // Generate an android resource module if this rule defines resources
+      // We don't want to generate one if this depends on a legacy resource rule through :resources
+      // In this case, the resource information is redundantly forwarded to this class for
+      // backwards compatibility, but the android_resource rule itself is already generating
+      // the android resource module
+      if (androidRuleIdeInfo.generateResourceClass && androidRuleIdeInfo.legacyResources == null) {
+        List<ArtifactLocation> nonGeneratedResources = Lists.newArrayList();
+        for (ArtifactLocation artifactLocation : androidRuleIdeInfo.resources) {
+          if (!artifactLocation.isGenerated()) {
+            nonGeneratedResources.add(artifactLocation);
+          }
+        }
+
+        // Only create a resource module if there are any non-generated resources
+        // Empty R classes or ones with only generated sources are added as jars
+        if (!nonGeneratedResources.isEmpty()) {
+          AndroidResourceModule.Builder builder = new AndroidResourceModule.Builder(rule.label);
+          workspaceBuilder.androidResourceModules.add(builder);
+
+          builder.addAllResources(nonGeneratedResources);
+
+          TransitiveResourceMap.TransitiveResourceInfo transitiveResourceInfo = transitiveResourceMap.get(rule.label);
+          for (ArtifactLocation artifactLocation : transitiveResourceInfo.transitiveResources) {
+            if (!artifactLocation.isGenerated()) {
+              builder.addTransitiveResource(artifactLocation);
+            }
+          }
+          for (Label resourceDependency : transitiveResourceInfo.transitiveResourceRules) {
+            if (!resourceDependency.equals(rule.label)) {
+              builder.addTransitiveResourceDependency(resourceDependency);
+            }
+          }
+        } else {
+          // 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.
+          boolean discardResourceJar = discardAndroidBinaryResourceJar && rule.kindIsOneOf(Kind.ANDROID_BINARY, Kind.ANDROID_TEST);
+          if (!discardResourceJar) {
+            LibraryArtifact resourceJar = androidRuleIdeInfo.resourceJar;
+            if (resourceJar != null) {
+              BlazeLibrary library = new BlazeLibrary(LibraryKey.fromJarFile(resourceJar.jar.getFile()), resourceJar);
+              workspaceBuilder.libraries.add(library);
+            }
+          }
+        }
+      }
+
+      LibraryArtifact idlJar = androidRuleIdeInfo.idlJar;
+      if (idlJar != null) {
+        BlazeLibrary library = new BlazeLibrary(LibraryKey.fromJarFile(idlJar.jar.getFile()), idlJar);
+        workspaceBuilder.libraries.add(library);
+      }
+    }
+  }
+
+  @Nullable
+  private BlazeLibrary createResourceLibrary(Collection<AndroidResourceModule> androidResourceModules) {
+    Set<File> result = Sets.newHashSet();
+    for (AndroidResourceModule androidResourceModule : androidResourceModules) {
+      result.addAll(androidResourceModule.transitiveResources);
+    }
+    for (AndroidResourceModule androidResourceModule : androidResourceModules) {
+      result.removeAll(androidResourceModule.resources);
+    }
+    if (!result.isEmpty()) {
+      return new BlazeLibrary(LibraryKey.forResourceLibrary(),
+                              ImmutableList.copyOf(result.stream().sorted().collect(Collectors.toList())));
+    }
+    return null;
+  }
+
+  @NotNull
+  private ImmutableList<AndroidResourceModule> buildAndroidResourceModules(WorkspaceBuilder workspaceBuilder) {
+    // Filter empty resource modules
+    Stream<AndroidResourceModule> androidResourceModuleStream = workspaceBuilder.androidResourceModules
+      .stream()
+      .map(AndroidResourceModule.Builder::build)
+      .filter(androidResourceModule -> !androidResourceModule.isEmpty())
+      .filter(androidResourceModule -> !androidResourceModule.resources.isEmpty());
+    List<AndroidResourceModule> androidResourceModules = androidResourceModuleStream.collect(Collectors.toList());
+
+    // Detect, filter, and warn about multiple R classes
+    Multimap<String, AndroidResourceModule> javaPackageToResourceModule = ArrayListMultimap.create();
+    for (AndroidResourceModule androidResourceModule : androidResourceModules) {
+      RuleIdeInfo rule = ruleMap.get(androidResourceModule.label);
+      AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
+      assert androidRuleIdeInfo != null;
+      javaPackageToResourceModule.put(androidRuleIdeInfo.resourceJavaPackage, androidResourceModule);
+    }
+
+    List<AndroidResourceModule> result = Lists.newArrayList();
+    for (String resourceJavaPackage : javaPackageToResourceModule.keySet()) {
+      Collection<AndroidResourceModule> androidResourceModulesWithJavaPackage = javaPackageToResourceModule.get(resourceJavaPackage);
+
+      if (androidResourceModulesWithJavaPackage.size() == 1) {
+        result.addAll(androidResourceModulesWithJavaPackage);
+      }
+      else {
+        StringBuilder messageBuilder = new StringBuilder();
+        messageBuilder.append("Multiple R classes generated with the same java package ").append(resourceJavaPackage).append(".R: ");
+        messageBuilder.append('\n');
+        for (AndroidResourceModule androidResourceModule : androidResourceModulesWithJavaPackage) {
+          messageBuilder.append("  ").append(androidResourceModule.label).append('\n');
+        }
+        String message = messageBuilder.toString();
+        context.output(new PerformanceWarning(message));
+        IssueOutput
+          .warn(message)
+          .submit(context);
+
+        result.add(selectBestAndroidResourceModule(androidResourceModulesWithJavaPackage));
+      }
+    }
+
+    Collections.sort(result, (lhs, rhs) -> Label.COMPARATOR.compare(lhs.label, rhs.label));
+    return ImmutableList.copyOf(result);
+  }
+
+  private AndroidResourceModule selectBestAndroidResourceModule(Collection<AndroidResourceModule> androidResourceModulesWithJavaPackage) {
+    return androidResourceModulesWithJavaPackage
+      .stream()
+      .max((lhs, rhs) -> ComparisonChain.start()
+        .compare(lhs.resources.size(), rhs.resources.size()) // Most resources wins
+        .compare(lhs.transitiveResources.size(), rhs.transitiveResources.size()) // Most transitive resources wins
+        .compare(rhs.label.toString().length(), lhs.label.toString().length()) // Shortest label wins - note lhs, rhs are flipped
+        .result())
+      .get();
+  }
+
+  static class WorkspaceBuilder {
+    List<AndroidResourceModule.Builder> androidResourceModules = Lists.newArrayList();
+    ImmutableList.Builder<BlazeLibrary> libraries = ImmutableList.builder();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/RuleIdeInfoTransitiveAggregator.java b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/RuleIdeInfoTransitiveAggregator.java
new file mode 100644
index 0000000..fd2d4d5
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/RuleIdeInfoTransitiveAggregator.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.android.sync.importer.aggregators;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Transitive aggregator for RuleIdeInfo.
+ */
+public abstract class RuleIdeInfoTransitiveAggregator<T> extends TransitiveAggregator<RuleIdeInfo, T> {
+  protected RuleIdeInfoTransitiveAggregator(@NotNull ImmutableMap<Label, RuleIdeInfo> ruleMap) {
+    super(ruleMap);
+  }
+
+  @Override
+  protected Iterable<Label> getDependencies(@NotNull RuleIdeInfo ruleIdeInfo) {
+    return ruleIdeInfo.dependencies;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveAggregator.java b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveAggregator.java
new file mode 100644
index 0000000..526a6b7
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveAggregator.java
@@ -0,0 +1,84 @@
+/*
+ * 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.aggregators;
+
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.model.primitives.Label;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Map;
+
+/**
+ * Peforms a transitive reduction on the rule
+ */
+public abstract class TransitiveAggregator<Rule, T> {
+  private Map<Label, T> labelToResult;
+
+  protected TransitiveAggregator(@NotNull Map<Label, Rule> ruleMap) {
+    this.labelToResult = Maps.newHashMap();
+    for (Label label : ruleMap.keySet()) {
+      aggregate(label, ruleMap);
+    }
+  }
+
+  @NotNull
+  protected T getOrDefault(@NotNull Label key, @NotNull T defaultValue) {
+    T result = labelToResult.get(key);
+    return result != null ? result : defaultValue;
+  }
+
+  @Nullable
+  private T aggregate(
+    @NotNull Label label,
+    @NotNull Map<Label, Rule> ruleMap) {
+    T result = labelToResult.get(label);
+    if (result != null) {
+      return result;
+    }
+
+    Rule rule = ruleMap.get(label);
+    if (rule == null) {
+      return null;
+    }
+
+    result = createForRule(rule);
+
+    for (Label depLabel : getDependencies(rule)) {
+      T depResult = aggregate(depLabel, ruleMap);
+      if (depResult != null) {
+        result = reduce(result, depResult);
+      }
+    }
+
+    labelToResult.put(label, result);
+    return result;
+  }
+
+  protected abstract Iterable<Label> getDependencies(@NotNull Rule rule);
+
+  /**
+   * Creates the initial value for a given rule.
+   */
+  @NotNull
+  protected abstract T createForRule(@NotNull Rule rule);
+
+  /**
+   * Reduces two values, sum + new value. May mutate value in place.
+   */
+  @NotNull
+  protected abstract T reduce(@NotNull T value, @NotNull T dependencyValue);
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveResourceMap.java b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveResourceMap.java
new file mode 100644
index 0000000..9bdd852
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/importer/aggregators/TransitiveResourceMap.java
@@ -0,0 +1,83 @@
+/*
+ * 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.aggregators;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Computes transitive resources.
+ */
+public class TransitiveResourceMap extends RuleIdeInfoTransitiveAggregator<TransitiveResourceMap.TransitiveResourceInfo> {
+  public static class TransitiveResourceInfo {
+    public static final TransitiveResourceInfo NO_RESOURCES = new TransitiveResourceInfo();
+    public final Set<ArtifactLocation> transitiveResources = Sets.newHashSet();
+    public final Set<Label> transitiveResourceRules = Sets.newHashSet();
+  }
+
+  public TransitiveResourceMap(@NotNull ImmutableMap<Label, RuleIdeInfo> ruleMap) {
+    super(ruleMap);
+  }
+
+  @Override
+  protected Iterable<Label> getDependencies(@NotNull RuleIdeInfo ruleIdeInfo) {
+    AndroidRuleIdeInfo androidRuleIdeInfo = ruleIdeInfo.androidRuleIdeInfo;
+    if (androidRuleIdeInfo != null && androidRuleIdeInfo.legacyResources != null) {
+      List<Label> result = Lists.newArrayList(super.getDependencies(ruleIdeInfo));
+      result.add(androidRuleIdeInfo.legacyResources);
+      return result;
+    }
+    return super.getDependencies(ruleIdeInfo);
+  }
+
+  @NotNull
+  public TransitiveResourceInfo get(@NotNull Label label) {
+    return getOrDefault(label, TransitiveResourceInfo.NO_RESOURCES);
+  }
+
+  @NotNull
+  @Override
+  protected TransitiveResourceInfo createForRule(@NotNull RuleIdeInfo ruleIdeInfo) {
+    TransitiveResourceInfo result = new TransitiveResourceInfo();
+    AndroidRuleIdeInfo androidRuleIdeInfo = ruleIdeInfo.androidRuleIdeInfo;
+    if (androidRuleIdeInfo == null) {
+      return result;
+    }
+    if (androidRuleIdeInfo.legacyResources != null) {
+      return result;
+    }
+    result.transitiveResources.addAll(androidRuleIdeInfo.resources);
+    result.transitiveResourceRules.add(ruleIdeInfo.label);
+    return result;
+  }
+
+  @NotNull
+  @Override
+  protected TransitiveResourceInfo reduce(@NotNull TransitiveResourceInfo value, @NotNull TransitiveResourceInfo dependencyValue) {
+    value.transitiveResources.addAll(dependencyValue.transitiveResources);
+    value.transitiveResourceRules.addAll(dependencyValue.transitiveResourceRules);
+    return value;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModule.java b/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModule.java
new file mode 100644
index 0000000..e8c61ff
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/AndroidResourceModule.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.android.sync.model;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.model.primitives.Label;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.File;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Immutable
+public final class AndroidResourceModule implements Serializable {
+  private static final long serialVersionUID = 5L;
+
+  public final Label label;
+  public final ImmutableCollection<File> resources;
+  public final ImmutableCollection<File> transitiveResources;
+  public final ImmutableCollection<Label> transitiveResourceDependencies;
+
+  public AndroidResourceModule(Label label,
+                               ImmutableCollection<File> resources,
+                               ImmutableCollection<File> transitiveResources,
+                               ImmutableCollection<Label> transitiveResourceDependencies) {
+    this.label = label;
+    this.resources = resources;
+    this.transitiveResources = transitiveResources;
+    this.transitiveResourceDependencies = transitiveResourceDependencies;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof AndroidResourceModule) {
+      AndroidResourceModule that = (AndroidResourceModule)o;
+      return Objects.equal(this.label, that.label)
+             && Objects.equal(this.resources, that.resources)
+             && Objects.equal(this.transitiveResources, that.transitiveResources)
+             && Objects.equal(this.transitiveResourceDependencies, that.transitiveResourceDependencies);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(
+      this.label,
+      this.resources,
+      this.transitiveResources,
+      this.transitiveResourceDependencies
+    );
+  }
+
+  @Override
+  public String toString() {
+    return "AndroidResourceModule{" + "\n"
+           + "  label: " + label + "\n"
+           + "  resources: " + resources + "\n"
+           + "  transitiveResources: " + transitiveResources + "\n"
+           + "  transitiveResourceDependencies: " + transitiveResourceDependencies + "\n"
+           + '}';
+  }
+
+  public static Builder builder(Label label) {
+    return new Builder(label);
+  }
+
+  public boolean isEmpty() {
+    return resources.isEmpty() && transitiveResources.isEmpty();
+  }
+
+  public static class Builder {
+    private final Label label;
+    private final Set<ArtifactLocation> resources = Sets.newHashSet();
+    private final Set<ArtifactLocation> transitiveResources = Sets.newHashSet();
+    private Set<Label> transitiveResourceDependencies = Sets.newHashSet();
+
+    public Builder(Label label) {
+      this.label = label;
+    }
+
+    public Builder addResource(ArtifactLocation resource) {
+      this.resources.add(resource);
+      return this;
+    }
+
+    public Builder addAllResources(List<ArtifactLocation> resources) {
+      this.resources.addAll(resources);
+      return this;
+    }
+
+    public Builder addResourceAndTransitiveResource(ArtifactLocation resource) {
+      this.resources.add(resource);
+      this.transitiveResources.add(resource);
+      return this;
+    }
+
+    public Builder addTransitiveResource(ArtifactLocation resource) {
+      this.transitiveResources.add(resource);
+      return this;
+    }
+
+    public Builder addTransitiveResourceDependency(Label dependency) {
+      this.transitiveResourceDependencies.add(dependency);
+      return this;
+    }
+
+    public Builder addTransitiveResourceDependency(String dependency) {
+      return addTransitiveResourceDependency(new Label(dependency));
+    }
+
+    @NotNull
+    public AndroidResourceModule build() {
+      return new AndroidResourceModule(
+        label,
+        ImmutableList.copyOf(
+          resources
+            .stream()
+            .map(ArtifactLocation::getFile)
+            .sorted()
+            .collect(Collectors.toList())),
+        ImmutableList.copyOf(
+          transitiveResources
+            .stream()
+            .map(ArtifactLocation::getFile)
+            .sorted()
+            .collect(Collectors.toList())),
+        ImmutableList.copyOf(
+          transitiveResourceDependencies
+            .stream()
+            .sorted()
+            .collect(Collectors.toList()))
+      );
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/AndroidSdkPlatform.java b/aswb/src/com/google/idea/blaze/android/sync/model/AndroidSdkPlatform.java
new file mode 100644
index 0000000..f38c80d
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/AndroidSdkPlatform.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.model;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.Serializable;
+
+/**
+ * Information about the android platform selected at sync time.
+ */
+@Immutable
+public class AndroidSdkPlatform implements Serializable {
+  public final String androidSdk;
+  public final int androidSdkLevel;
+
+  public AndroidSdkPlatform(String androidSdk, int androidSdkLevel) {
+    this.androidSdk = androidSdk;
+    this.androidSdkLevel = androidSdkLevel;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidImportResult.java b/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidImportResult.java
new file mode 100644
index 0000000..e134399
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidImportResult.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync.model;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.Serializable;
+
+/**
+ * The result of a blaze import operation.
+ */
+@Immutable
+public class BlazeAndroidImportResult implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final ImmutableCollection<AndroidResourceModule> androidResourceModules;
+  public final ImmutableCollection<BlazeLibrary> libraries;
+
+  public BlazeAndroidImportResult(
+    ImmutableCollection<AndroidResourceModule> androidResourceModules,
+    ImmutableCollection<BlazeLibrary> libraries) {
+    this.androidResourceModules = androidResourceModules;
+    this.libraries = libraries;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidSyncData.java b/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidSyncData.java
new file mode 100644
index 0000000..c0f529b
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/BlazeAndroidSyncData.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync.model;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import java.io.Serializable;
+
+/**
+ * Sync data for the Android plugin.
+ */
+@Immutable
+public class BlazeAndroidSyncData implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final BlazeAndroidImportResult importResult;
+  @Nullable public final AndroidSdkPlatform androidSdkPlatform;
+
+  public BlazeAndroidSyncData(BlazeAndroidImportResult importResult,
+                              @Nullable AndroidSdkPlatform androidSdkPlatform) {
+    this.importResult = importResult;
+    this.androidSdkPlatform = androidSdkPlatform;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java
new file mode 100644
index 0000000..48b4dcc
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeAndroidModel.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync.model.idea;
+
+import com.android.builder.model.SourceProvider;
+import com.android.sdklib.AndroidVersion;
+import com.android.tools.idea.model.AndroidModel;
+import com.android.tools.idea.model.ClassJarProvider;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.android.manifest.ManifestParser;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.android.dom.manifest.Manifest;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Contains Android-Blaze related state necessary for configuring an IDEA project based on a
+ * user-selected build variant.
+ */
+public class BlazeAndroidModel implements AndroidModel {
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidModel.class);
+
+  private Project project;
+  private Module module;
+  private final File rootDirPath;
+  private final SourceProvider sourceProvider;
+  private final List<SourceProvider> sourceProviders; // Singleton list of sourceProvider
+  private final File moduleManifest;
+  private final String resourceJavaPackage;
+  private final int androidSdkApiLevel;
+
+  /**
+   * Creates a new {@link BlazeAndroidModel}.
+   */
+  public BlazeAndroidModel(
+    Project project,
+    Module module,
+    File rootDirPath,
+    SourceProvider sourceProvider,
+    File moduleManifest,
+    String resourceJavaPackage,
+    int androidSdkApiLevel) {
+    this.project = project;
+    this.module = module;
+    this.rootDirPath = rootDirPath;
+    this.sourceProvider = sourceProvider;
+    this.sourceProviders = ImmutableList.of(sourceProvider);
+    this.moduleManifest = moduleManifest;
+    this.resourceJavaPackage = resourceJavaPackage;
+    this.androidSdkApiLevel = androidSdkApiLevel;
+  }
+
+  @NotNull
+  @Override
+  public SourceProvider getDefaultSourceProvider() {
+    return sourceProvider;
+  }
+
+  @NotNull
+  @Override
+  public List<SourceProvider> getActiveSourceProviders() {
+    return sourceProviders;
+  }
+
+  @NotNull
+  @Override
+  public List<SourceProvider> getTestSourceProviders() {
+    return sourceProviders;
+  }
+
+  @NotNull
+  @Override
+  public List<SourceProvider> getAllSourceProviders() {
+    return sourceProviders;
+  }
+
+  @Override
+  @NotNull
+  public String getApplicationId() {
+    String result = null;
+    Manifest manifest = ManifestParser.getInstance(project).getManifest(moduleManifest);
+    if (manifest != null) {
+      result = manifest.getPackage().getValue();
+    }
+    if (result == null) {
+      result = resourceJavaPackage;
+    }
+    return result;
+  }
+
+  @NotNull
+  @Override
+  public Set<String> getAllApplicationIds() {
+    Set<String> applicationIds = Sets.newHashSet();
+    applicationIds.add(getApplicationId());
+    return applicationIds;
+  }
+
+  @Override
+  public boolean overridesManifestPackage() {
+    return false;
+  }
+
+  @Override
+  public Boolean isDebuggable() {
+    return true;
+  }
+
+  @Override
+  @Nullable
+  public AndroidVersion getMinSdkVersion() {
+    return new AndroidVersion(androidSdkApiLevel, null);
+  }
+
+  @Nullable
+  @Override
+  public AndroidVersion getRuntimeMinSdkVersion() {
+    return getMinSdkVersion();
+  }
+
+  @Nullable
+  @Override
+  public AndroidVersion getTargetSdkVersion() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Integer getVersionCode() {
+    return null;
+  }
+
+  @NotNull
+  @Override
+  public File getRootDirPath() {
+    return rootDirPath;
+  }
+
+  @Override
+  public boolean isGenerated(@NotNull VirtualFile file) {
+    return false;
+  }
+
+  @NotNull
+  @Override
+  public VirtualFile getRootDir() {
+    File rootDirPath = getRootDirPath();
+    VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(rootDirPath);
+    assert virtualFile != null;
+    return virtualFile;
+  }
+
+  @Override
+  public boolean getDataBindingEnabled() {
+    return false;
+  }
+
+  @Override
+  @NotNull
+  public ClassJarProvider getClassJarProvider() {
+    return new NullClassJarProvider();
+  }
+
+  @Override
+  @Nullable
+  public Long getLastBuildTimestamp(@NotNull Project project) {
+    // TODO(jvoung): Coordinate with blaze build actions to be able determine last build time.
+    return null;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
new file mode 100644
index 0000000..7719c95
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/BlazeClassJarProvider.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync.model.idea;
+
+import com.android.SdkConstants;
+import com.android.tools.idea.model.ClassJarProvider;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.JarFileSystem;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.util.containers.OrderedSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.List;
+
+public class BlazeClassJarProvider extends ClassJarProvider {
+
+  private @NotNull final Project project;
+
+  public BlazeClassJarProvider(@NotNull final Project project){
+    this.project = project;
+  }
+
+  @Override
+  @Nullable
+  public VirtualFile findModuleClassFile(@NotNull String className, @NotNull Module module) {
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
+    LocalFileSystem localVfs = LocalFileSystem.getInstance();
+    String classNamePath = className.replace('.', File.separatorChar) + SdkConstants.DOT_CLASS;
+    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    if (syncData == null) {
+      return null;
+    }
+    for (File runtimeJar : syncData.importResult.buildOutputJars) {
+      VirtualFile runtimeJarVF = localVfs.findFileByIoFile(runtimeJar);
+      if (runtimeJarVF == null) {
+        continue;
+      }
+      VirtualFile classFile = findClassInJar(runtimeJarVF, classNamePath);
+      if (classFile != null) {
+        return classFile;
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  private static VirtualFile findClassInJar(@NotNull final VirtualFile runtimeJar,
+                                            @NotNull String classNamePath) {
+    VirtualFile jarRoot = JarFileSystem.getInstance().getJarRootForLocalFile(runtimeJar);
+    if (jarRoot == null) {
+      return null;
+    }
+    return jarRoot.findFileByRelativePath(classNamePath);
+  }
+
+  @Override
+  @NotNull
+  public List<VirtualFile> getModuleExternalLibraries(@NotNull Module module) {
+    OrderedSet<VirtualFile> results = new OrderedSet<VirtualFile>();
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return results;
+    }
+    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    if (syncData == null) {
+      return null;
+    }
+    LocalFileSystem localVfs = LocalFileSystem.getInstance();
+    for (BlazeLibrary blazeLibrary : syncData.importResult.libraries.values()) {
+      LibraryArtifact libraryArtifact = blazeLibrary.getLibraryArtifact();
+      if (libraryArtifact == null) {
+        continue;
+      }
+      ArtifactLocation runtimeJar = libraryArtifact.runtimeJar;
+      if (runtimeJar == null) {
+        continue;
+      }
+      VirtualFile libVF = localVfs.findFileByIoFile(runtimeJar.getFile());
+      if (libVF == null) {
+        continue;
+      }
+      results.add(libVF);
+    }
+
+    return results;
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/NullClassJarProvider.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/NullClassJarProvider.java
new file mode 100644
index 0000000..969428f
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/NullClassJarProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.android.sync.model.idea;
+
+import com.android.tools.idea.model.ClassJarProvider;
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Returns no class jars. Used to disable the layout editor
+ * loading jars.
+ */
+public class NullClassJarProvider extends ClassJarProvider {
+  @Nullable
+  @Override
+  public VirtualFile findModuleClassFile(@NotNull String className, @NotNull Module module) {
+    return null;
+  }
+
+  @NotNull
+  @Override
+  public List<VirtualFile> getModuleExternalLibraries(@NotNull Module module) {
+    return ImmutableList.of();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/model/idea/SourceProviderImpl.java b/aswb/src/com/google/idea/blaze/android/sync/model/idea/SourceProviderImpl.java
new file mode 100644
index 0000000..427f0fa
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/model/idea/SourceProviderImpl.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.model.idea;
+
+import com.android.builder.model.SourceProvider;
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.diagnostic.Logger;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Implementation of SourceProvider that is serializable. Objects used in the DSL cannot be
+ * serialized.
+ */
+public class SourceProviderImpl implements SourceProvider, Serializable {
+
+  private static final long serialVersionUID = 1L;
+
+  private final String name;
+  private final File manifestFile;
+  private final Collection<File> resDirs;
+
+  public SourceProviderImpl(String name,
+                            File manifestFile,
+                            Collection<File> resDirs) {
+    this.name = name;
+    this.manifestFile = manifestFile;
+    this.resDirs = resDirs;
+  }
+
+  @Override
+  public String getName() {
+    return name;
+  }
+
+  @Override
+  public File getManifestFile() {
+    return manifestFile;
+  }
+
+  @Override
+  public Collection<File> getJavaDirectories() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<File> getResourcesDirectories() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<File> getAidlDirectories() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<File> getRenderscriptDirectories() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<File> getCDirectories() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<File> getCppDirectories() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<File> getResDirectories() {
+    return resDirs;
+  }
+
+  @Override
+  public Collection<File> getAssetsDirectories() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<File> getJniLibsDirectories() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<File> getShadersDirectories() {
+    return ImmutableList.of();
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.java
new file mode 100755
index 0000000..bae295d
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/AndroidFacetModuleCustomizer.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.sync.projectstructure;
+
+import com.android.builder.model.AndroidProject;
+import com.intellij.facet.FacetManager;
+import com.intellij.facet.ModifiableFacetModel;
+import com.intellij.openapi.module.Module;
+import org.jetbrains.android.facet.AndroidFacet;
+import org.jetbrains.jps.android.model.impl.JpsAndroidModuleProperties;
+
+/**
+ * Adds the Android facet to modules imported from {@link AndroidProject}s.
+ */
+public class AndroidFacetModuleCustomizer {
+
+  public static void createAndroidFacet(Module module) {
+    AndroidFacet facet = AndroidFacet.getInstance(module);
+    if (facet != null) {
+      configureFacet(facet);
+    }
+    else {
+      // Module does not have Android facet. Create one and add it.
+      FacetManager facetManager = FacetManager.getInstance(module);
+      ModifiableFacetModel model = facetManager.createModifiableModel();
+      try {
+        facet = facetManager.createFacet(AndroidFacet.getFacetType(), AndroidFacet.NAME, null);
+        model.addFacet(facet);
+        configureFacet(facet);
+      }
+      finally {
+        model.commit();
+      }
+    }
+  }
+
+  public static void removeAndroidFacet(Module module) {
+    AndroidFacet facet = AndroidFacet.getInstance(module);
+    if (facet != null) {
+      ModifiableFacetModel facetModel = FacetManager.getInstance(module).createModifiableModel();
+      facetModel.removeFacet(facet);
+      facetModel.commit();
+    }
+  }
+
+  private static void configureFacet(AndroidFacet facet) {
+    JpsAndroidModuleProperties facetState = facet.getProperties();
+    facetState.ALLOW_USER_CONFIGURATION = false;
+    facetState.LIBRARY_PROJECT = true;
+    facetState.MANIFEST_FILE_RELATIVE_PATH = "";
+    facetState.RES_FOLDER_RELATIVE_PATH = "";
+    facetState.ASSETS_FOLDER_RELATIVE_PATH = "";
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java
new file mode 100644
index 0000000..d411447
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/BlazeAndroidProjectStructureSyncer.java
@@ -0,0 +1,350 @@
+/*
+ * 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.projectstructure;
+
+import com.android.builder.model.SourceProvider;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.android.resources.LightResourceClassService;
+import com.google.idea.blaze.android.run.BlazeAndroidRunConfiguration;
+import com.google.idea.blaze.android.sync.AndroidSdkPlatformSyncer;
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.AndroidSdkPlatform;
+import com.google.idea.blaze.android.sync.model.BlazeAndroidSyncData;
+import com.google.idea.blaze.android.sync.model.idea.BlazeAndroidModel;
+import com.google.idea.blaze.android.sync.model.idea.SourceProviderImpl;
+import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.module.StdModuleTypes;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import org.jetbrains.android.facet.AndroidFacet;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Updates the IDE's project structure.
+ */
+public class BlazeAndroidProjectStructureSyncer {
+
+  public static void updateProjectStructure(Project project,
+                                            BlazeContext context,
+                                            WorkspaceRoot workspaceRoot,
+                                            ProjectViewSet projectViewSet,
+                                            BlazeProjectData blazeProjectData,
+                                            BlazeSyncPlugin.ModuleEditor moduleEditor,
+                                            Module workspaceModule,
+                                            ModifiableRootModel workspaceModifiableModel,
+                                            boolean isAndroidWorkspace) {
+    LightResourceClassService.Builder rClassBuilder = new LightResourceClassService.Builder(project);
+
+    if (isAndroidWorkspace) {
+      BlazeAndroidSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidSyncData.class);
+      if (syncData == null) {
+        return;
+      }
+
+      AndroidSdkPlatform androidSdkPlatform = syncData.androidSdkPlatform;
+      if (androidSdkPlatform != null) {
+        int totalOrderEntries = 0;
+
+        // Create the workspace module
+        updateWorkspaceModule(
+          project,
+          workspaceRoot,
+          workspaceModule,
+          androidSdkPlatform
+        );
+
+        // Create android resource modules
+        // Because we're setting up dependencies, the modules have to exist before we configure them
+        Map<Label, AndroidResourceModule> labelToAndroidResourceModule = Maps.newHashMap();
+        for (AndroidResourceModule androidResourceModule : syncData.importResult.androidResourceModules) {
+          labelToAndroidResourceModule.put(androidResourceModule.label, androidResourceModule);
+          String moduleName = moduleNameForAndroidModule(androidResourceModule.label);
+          moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
+        }
+
+        // Configure android resource modules
+        for (AndroidResourceModule androidResourceModule : labelToAndroidResourceModule.values()) {
+          RuleIdeInfo rule = blazeProjectData.ruleMap.get(androidResourceModule.label);
+          AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
+          assert androidRuleIdeInfo != null;
+
+          String moduleName = moduleNameForAndroidModule(rule.label);
+          Module module = moduleEditor.findModule(moduleName);
+          assert module != null;
+          ModifiableRootModel modifiableRootModel = moduleEditor.editModule(module);
+
+          updateAndroidRuleModule(
+            project,
+            workspaceRoot,
+            androidSdkPlatform,
+            rule,
+            module,
+            modifiableRootModel,
+            androidResourceModule
+          );
+
+          for (Label resourceDependency : androidResourceModule.transitiveResourceDependencies) {
+            if (!labelToAndroidResourceModule.containsKey(resourceDependency)) {
+              continue;
+            }
+            String dependencyModuleName = moduleNameForAndroidModule(resourceDependency);
+            Module dependency = moduleEditor.findModule(dependencyModuleName);
+            if (dependency == null) {
+              continue;
+            }
+            modifiableRootModel.addModuleOrderEntry(dependency);
+            ++totalOrderEntries;
+          }
+          rClassBuilder.addRClass(androidRuleIdeInfo.resourceJavaPackage, module);
+          // Add a dependency from the workspace to the resource module
+          workspaceModifiableModel.addModuleOrderEntry(module);
+        }
+
+        // Collect potential android run configuration targets
+        Set<Label> runConfigurationModuleTargets = Sets.newHashSet();
+
+        // Get all explicitly mentioned targets
+        // Doing this now will cut down on root changes later
+        for (TargetExpression targetExpression : projectViewSet.listItems(TargetSection.KEY)) {
+          if (!(targetExpression instanceof Label)) {
+            continue;
+          }
+          Label label = (Label)targetExpression;
+          runConfigurationModuleTargets.add(label);
+        }
+        // Get any pre-existing targets
+        for (RunConfiguration runConfiguration : RunManager.getInstance(project).getAllConfigurationsList()) {
+          if (!(runConfiguration instanceof BlazeAndroidRunConfiguration)) {
+            continue;
+          }
+          BlazeAndroidRunConfiguration blazeAndroidRunConfiguration = (BlazeAndroidRunConfiguration)runConfiguration;
+          runConfigurationModuleTargets.add(blazeAndroidRunConfiguration.getTarget());
+        }
+
+        int totalRunConfigurationModules = 0;
+        for (Label label : runConfigurationModuleTargets) {
+          // If it's a resource module, it will already have been created
+          if (labelToAndroidResourceModule.containsKey(label)) {
+            continue;
+          }
+          // Ensure the label is a supported android rule that exists
+          RuleIdeInfo rule = blazeProjectData.ruleMap.get(label);
+          if (rule == null) {
+            continue;
+          }
+          if (!rule.kindIsOneOf(Kind.ANDROID_BINARY, Kind.ANDROID_TEST)) {
+            continue;
+          }
+
+          String moduleName = moduleNameForAndroidModule(rule.label);
+          Module module = moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
+          ModifiableRootModel modifiableRootModel = moduleEditor.editModule(module);
+          updateAndroidRuleModule(
+            project,
+            workspaceRoot,
+            androidSdkPlatform,
+            rule,
+            module,
+            modifiableRootModel,
+            null
+          );
+          ++totalRunConfigurationModules;
+        }
+
+        context.output(new PrintOutput(String.format(
+          "Android resource module count: %d, run config modules: %d, order entries: %d",
+          syncData.importResult.androidResourceModules.size(),
+          totalRunConfigurationModules,
+          totalOrderEntries
+        )));
+      }
+    } else {
+      AndroidFacetModuleCustomizer.removeAndroidFacet(workspaceModule);
+    }
+
+    LightResourceClassService.getInstance(project).installRClasses(rClassBuilder);
+  }
+
+  /**
+   * Ensures a suitable module exists for the given android target.
+   */
+  @Nullable
+  public static Module ensureRunConfigurationModule(Project project, Label target) {
+    String moduleName = moduleNameForAndroidModule(target);
+    Module module = ModuleManager.getInstance(project).findModuleByName(moduleName);
+    if (module != null) {
+      return module;
+    }
+
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
+    AndroidSdkPlatform androidSdkPlatform = AndroidSdkPlatformSyncer.getAndroidSdkPlatform(blazeProjectData);
+    if (androidSdkPlatform == null) {
+      return null;
+    }
+    RuleIdeInfo rule = blazeProjectData.ruleMap.get(target);
+    if (rule == null) {
+      return null;
+    }
+    if (rule.androidRuleIdeInfo == null) {
+      return null;
+    }
+
+    BlazeSyncPlugin.ModuleEditor moduleEditor = BlazeProjectDataManager.getInstance(project).editModules();
+    Module newModule = moduleEditor.createModule(moduleName, StdModuleTypes.JAVA);
+    ModifiableRootModel modifiableRootModel = moduleEditor.editModule(newModule);
+
+    ApplicationManager.getApplication().runWriteAction(() -> {
+      updateAndroidRuleModule(
+        project,
+        workspaceRoot,
+        androidSdkPlatform,
+        rule,
+        newModule,
+        modifiableRootModel,
+        null
+      );
+      moduleEditor.commit();
+    });
+    return newModule;
+  }
+
+  public static String moduleNameForAndroidModule(Label label) {
+    return label.toString()
+      .substring(2) // Skip initial "//"
+      .replace('/', '.')
+      .replace(':', '.');
+  }
+
+  /**
+   * Updates the shared workspace module with android info.
+   */
+  private static void updateWorkspaceModule(Project project,
+                                           WorkspaceRoot workspaceRoot,
+                                           Module workspaceModule,
+                                           AndroidSdkPlatform androidSdkPlatform) {
+    File moduleDirectory = workspaceRoot.directory();
+    File manifest = new File(workspaceRoot.directory(), "AndroidManifest.xml");
+    String resourceJavaPackage = ":workspace";
+    ImmutableList<File> transitiveResources = ImmutableList.of();
+
+    createAndroidModel(
+      project,
+      androidSdkPlatform,
+      workspaceModule,
+      moduleDirectory,
+      manifest,
+      resourceJavaPackage,
+      transitiveResources
+    );
+  }
+
+  /**
+   * Updates a module from an android rule.
+   */
+  private static void updateAndroidRuleModule(Project project,
+                                              WorkspaceRoot workspaceRoot,
+                                              AndroidSdkPlatform androidSdkPlatform,
+                                              RuleIdeInfo rule,
+                                              Module module,
+                                              ModifiableRootModel modifiableRootModel,
+                                              @Nullable AndroidResourceModule androidResourceModule) {
+
+    ImmutableCollection<File> resources = androidResourceModule != null
+                                          ? androidResourceModule.resources
+                                          : ImmutableList.of();
+    ImmutableCollection<File> transitiveResources = androidResourceModule != null
+                                                    ? androidResourceModule.transitiveResources
+                                                    : ImmutableList.of();
+
+    AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
+    assert androidRuleIdeInfo != null;
+
+    File moduleDirectory = workspaceRoot.fileForPath(rule.label.blazePackage());
+    ArtifactLocation manifestArtifactLocation = androidRuleIdeInfo.manifest;
+    File manifest = manifestArtifactLocation != null
+                    ? manifestArtifactLocation.getFile()
+                    : new File(moduleDirectory, "AndroidManifest.xml");
+    String resourceJavaPackage = androidRuleIdeInfo.resourceJavaPackage;
+    ResourceModuleContentRootCustomizer.setupContentRoots(modifiableRootModel, resources);
+
+    createAndroidModel(
+      project,
+      androidSdkPlatform,
+      module,
+      moduleDirectory,
+      manifest,
+      resourceJavaPackage,
+      transitiveResources
+    );
+  }
+
+  private static void createAndroidModel(Project project,
+                                         AndroidSdkPlatform androidSdkPlatform,
+                                         Module module,
+                                         File moduleDirectory,
+                                         File manifest,
+                                         String resourceJavaPackage,
+                                         ImmutableCollection<File> transitiveResources) {
+    AndroidFacetModuleCustomizer.createAndroidFacet(module);
+    SourceProvider sourceProvider = new SourceProviderImpl(
+      module.getName(),
+      manifest,
+      transitiveResources
+    );
+    BlazeAndroidModel androidModel = new BlazeAndroidModel(
+      project,
+      module,
+      moduleDirectory,
+      sourceProvider,
+      manifest,
+      resourceJavaPackage,
+      androidSdkPlatform.androidSdkLevel
+    );
+    AndroidFacet facet = AndroidFacet.getInstance(module);
+    if (facet != null) {
+      facet.setAndroidModel(androidModel);
+    }
+  }
+}
diff --git a/aswb/src/com/google/idea/blaze/android/sync/projectstructure/ResourceModuleContentRootCustomizer.java b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/ResourceModuleContentRootCustomizer.java
new file mode 100644
index 0000000..adf17ff
--- /dev/null
+++ b/aswb/src/com/google/idea/blaze/android/sync/projectstructure/ResourceModuleContentRootCustomizer.java
@@ -0,0 +1,59 @@
+/*
+ * 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.projectstructure;
+
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.util.io.URLUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.jps.model.java.JavaResourceRootType;
+
+import java.io.File;
+import java.util.Collection;
+
+public class ResourceModuleContentRootCustomizer {
+
+  public static void setupContentRoots(
+    @NotNull ModifiableRootModel model,
+    @NotNull Collection<File> resources) {
+    for (ContentEntry contentEntry : model.getContentEntries()) {
+      model.removeContentEntry(contentEntry);
+    }
+
+    for (File resource : resources) {
+      ContentEntry contentEntry = model.addContentEntry(pathToUrl(resource.getPath()));
+      contentEntry.addSourceFolder(pathToUrl(resource.getPath()), JavaResourceRootType.RESOURCE);
+    }
+  }
+
+  @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/aswb/src/icons/BlazeAndroidIcons.java b/aswb/src/icons/BlazeAndroidIcons.java
new file mode 100644
index 0000000..7f14ec5
--- /dev/null
+++ b/aswb/src/icons/BlazeAndroidIcons.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 icons;
+
+import com.intellij.openapi.util.IconLoader;
+
+import javax.swing.*;
+
+/**
+ * Class to manage icons used by the Blaze plugin.
+ */
+public class BlazeAndroidIcons {
+
+  public static final Icon MobileInstallRun = load("/aswb/resources/icons/mobileInstallRun.png"); // 16x16
+  public static final Icon MobileInstallDebug = load("/aswb/resources/icons/mobileInstallDebug.png"); // 16x16
+  public static final Icon Crow = load("/aswb/resources/icons/crow.png"); // 16x16
+  public static final Icon CrowToolWindow = load("/aswb/resources/icons/crowToolWindow.png"); // 13x13
+
+  private static Icon load(String path) {
+    return IconLoader.getIcon(path, BlazeAndroidIcons.class);
+  }
+}
diff --git a/aswb/tests/unittests/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtilsTest.java b/aswb/tests/unittests/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtilsTest.java
new file mode 100644
index 0000000..d2490ed
--- /dev/null
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/resources/actions/BlazeCreateResourceUtilsTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.resources.actions;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.intellij.mock.MockVirtualFile;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for BlazeCreateResourceUtils.
+ */
+@RunWith(JUnit4.class)
+public class BlazeCreateResourceUtilsTest extends BlazeTestCase {
+
+  // Set up a mock directory tree:
+  // foo/src/
+  //         res/
+  //             values-fr-rCA/
+  //                           misc/
+  //                           foo.xml
+  //             layout/
+  //         A.java
+  private MockVirtualFile xmlFile = file("foo.xml");
+  private MockVirtualFile outOfPlaceSubdir = dir("misc");
+  private MockVirtualFile valuesDir = dir("values-fr-rCA", xmlFile, outOfPlaceSubdir);
+  private MockVirtualFile layoutDir = dir("layout");
+  private MockVirtualFile resDir = dir("res", valuesDir, layoutDir);
+  private MockVirtualFile javaFile = file("A.java");
+  private MockVirtualFile srcDir = dir("src", resDir, javaFile);
+  private MockVirtualFile baseDir = dir("foo", srcDir);
+
+  @Test
+  public void getDirectoryFromContextResDirectory() {
+    VirtualFile dir = BlazeCreateResourceUtils.getResDirFromDataContext(resDir);
+    assertThat(dir).isNotNull();
+    assertThat(dir.getName()).isEqualTo("res");
+  }
+
+  @Test
+  public void getDirectoryFromContextResSubdirectory() {
+    VirtualFile dir = BlazeCreateResourceUtils.getResDirFromDataContext(valuesDir);
+    assertThat(dir).isNotNull();
+    assertThat(dir.getName()).isEqualTo("res");
+  }
+
+  @Test
+  public void getDirectoryFromContextResFile() {
+    VirtualFile dir = BlazeCreateResourceUtils.getResDirFromDataContext(xmlFile);
+    assertThat(dir).isNotNull();
+    assertThat(dir.getName()).isEqualTo("res");
+  }
+
+  @Test
+  public void getDirectoryFromContextOutOfPlaceSubdir() {
+    // We don't try to guess the res/ ancestor from non-standard directory setups.
+    VirtualFile dir = BlazeCreateResourceUtils.getResDirFromDataContext(outOfPlaceSubdir);
+    assertThat(dir).isNull();
+  }
+
+  @Test
+  public void getDirectoryFromContextJavaFile() {
+    // This is just the first cut, where it isn't obvious that the A.java file is associated with the
+    // neighboring res directory. We'll have a second pass that looks at the rule map for possible choices.
+    VirtualFile dir = BlazeCreateResourceUtils.getResDirFromDataContext(javaFile);
+    assertThat(dir).isNull();
+  }
+
+  private static MockVirtualFile dir(@NotNull String name, MockVirtualFile... children) {
+    MockVirtualFile dir = new MockVirtualFile(true, name);
+    for (MockVirtualFile child : children) {
+      dir.addChild(child);
+    }
+    return dir;
+  }
+
+  private static MockVirtualFile file(@NotNull String name) {
+    return new MockVirtualFile(name);
+  }
+}
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
new file mode 100644
index 0000000..107ab27
--- /dev/null
+++ b/aswb/tests/unittests/com/google/idea/blaze/android/sync/importer/BlazeAndroidWorkspaceImporterTest.java
@@ -0,0 +1,537 @@
+/*
+ * 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;
+
+import com.google.idea.blaze.android.sync.model.AndroidResourceModule;
+import com.google.idea.blaze.android.sync.model.BlazeAndroidImportResult;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.executor.MockBlazeExecutor;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.ideinfo.*;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.File;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests for BlazeAndroidWorkspaceImporter
+ */
+public class BlazeAndroidWorkspaceImporterTest extends BlazeTestCase {
+
+  private String FAKE_ROOT = "/root";
+  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_ROOT));
+
+  private static final String FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT =
+    "blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
+
+  private static final String FAKE_GEN_ROOT =
+    "/abs_root/_blaze_user/8093958afcfde6c33d08b621dfaa4e09/root/"
+    + FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT;
+
+  private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS = new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+
+  private BlazeContext context;
+  private ErrorCollector errorCollector = new ErrorCollector();
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    MockExperimentService mockExperimentService = new MockExperimentService();
+    applicationServices.register(ExperimentService.class, mockExperimentService);
+
+    BlazeExecutor blazeExecutor = new MockBlazeExecutor();
+    applicationServices.register(BlazeExecutor.class, blazeExecutor);
+
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
+
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+  }
+
+  BlazeAndroidImportResult importWorkspace(
+    WorkspaceRoot workspaceRoot,
+    RuleMapBuilder ruleMapBuilder,
+    ProjectView projectView) {
+
+    ProjectViewSet projectViewSet = ProjectViewSet.builder().add(projectView).build();
+
+    BlazeAndroidWorkspaceImporter workspaceImporter = new BlazeAndroidWorkspaceImporter(
+      project,
+      context,
+      workspaceRoot,
+      projectViewSet,
+      ruleMapBuilder.build()
+    );
+
+    return workspaceImporter.importWorkspace();
+  }
+
+  /**
+   * Test that a two packages use the same un-imported android_library
+   */
+  @Test
+  public void testResourceInheritance() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("javatests/com/google/android/apps/example"))))
+      .build();
+
+    /**
+     * Deps are project -> lib0 -> lib1 -> shared
+     *          project -> shared
+     */
+
+    RuleMapBuilder response = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example/lib0:lib0")
+          .setKind("android_library")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/lib0/BUILD"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/lib0/SharedActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/lib0/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/lib0/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example.lib0"))
+          .addDependency("//java/com/google/android/apps/example/lib1:lib1")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/lib0/lib0.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib0/lib0.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example/lib1:lib1")
+          .setKind("android_library")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/lib1/BUILD"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/lib1/SharedActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/lib1/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/lib1/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example.lib1"))
+          .addDependency("//java/com/google/android/libraries/shared:shared")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/lib1/lib1.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib1/lib1.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setKind("android_binary")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .addDependency("//java/com/google/android/apps/example/lib0:lib0")
+          .addDependency("//java/com/google/android/libraries/shared:shared")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/example_debug.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/example_debug.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/libraries/shared:shared")
+          .setBuildFile(sourceRoot("java/com/google/android/libraries/shared/BUILD"))
+          .setKind("android_library")
+          .addSource(sourceRoot("java/com/google/android/libraries/shared/SharedActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/libraries/shared/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/libraries/shared/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.libraries.shared"))
+          .setBuildFile(sourceRoot("java/com/google/android/libraries/shared/BUILD"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/libraries/shared.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/libraries/shared.jar")))));
+
+    BlazeAndroidImportResult result = importWorkspace(
+      workspaceRoot,
+      response,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.androidResourceModules).containsExactly(
+      AndroidResourceModule.builder(new Label("//java/com/google/android/apps/example:example_debug"))
+        .addResourceAndTransitiveResource(sourceRoot("java/com/google/android/apps/example/res"))
+        .addTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib0/res"))
+        .addTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib1/res"))
+        .addTransitiveResource(sourceRoot("java/com/google/android/libraries/shared/res"))
+        .addTransitiveResourceDependency("//java/com/google/android/apps/example/lib0:lib0")
+        .addTransitiveResourceDependency("//java/com/google/android/apps/example/lib1:lib1")
+        .addTransitiveResourceDependency("//java/com/google/android/libraries/shared:shared")
+        .build(),
+      AndroidResourceModule.builder(new Label("//java/com/google/android/apps/example/lib0:lib0"))
+        .addResourceAndTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib0/res"))
+        .addTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib1/res"))
+        .addTransitiveResource(sourceRoot("java/com/google/android/libraries/shared/res"))
+        .addTransitiveResourceDependency("//java/com/google/android/apps/example/lib1:lib1")
+        .addTransitiveResourceDependency("//java/com/google/android/libraries/shared:shared")
+        .build(),
+      AndroidResourceModule.builder(new Label("//java/com/google/android/apps/example/lib1:lib1"))
+        .addResourceAndTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib1/res"))
+        .addTransitiveResource(sourceRoot("java/com/google/android/libraries/shared/res"))
+        .addTransitiveResourceDependency("//java/com/google/android/libraries/shared:shared")
+        .build()
+    );
+  }
+
+  /**
+   * Test adding empty resource modules as jars.
+   */
+  @Test
+  public void testEmptyResourceModuleIsAddedAsJar() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("javatests/com/google/android/apps/example"))))
+      .build();
+
+    /**
+     * Deps are project -> lib0 (no res) -> lib1 (has res)
+     *                                    \
+     *                                     -> lib2 (has res)
+     */
+    RuleMapBuilder response = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example/lib0:lib0")
+          .setKind("android_library")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/lib0/BUILD"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/lib0/SharedActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/lib0/AndroidManifest.xml"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example.lib0")
+                            .setResourceJar(LibraryArtifact.builder()
+                                              .setJar(genRoot("java/com/google/android/apps/example/lib0/lib0_resources.jar"))
+                                              .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib0/lib0_resources.jar"))))
+          .addDependency("//java/com/google/android/apps/example/lib1:lib1")
+          .addDependency("//java/com/google/android/apps/example/lib2:lib2")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/lib0/lib0.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib0/lib0.jar")))
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/lib0/lib0_resources.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib0/lib0_resources.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example/lib1:lib1")
+          .setKind("android_library")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/lib1/BUILD"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/lib1/SharedActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/lib1/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/lib1/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example.lib1")
+                            .setResourceJar(LibraryArtifact.builder()
+                                              .setJar(genRoot("java/com/google/android/apps/example/lib1/li11_resources.jar"))
+                                              .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib1/lib1_resources.jar"))))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/lib1/lib1.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib1/lib1.jar")))
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/lib1/lib1_resources.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib1/lib1_resources.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example/lib2:lib2")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/lib2/BUILD"))
+          .setKind("android_library")
+          .addSource(sourceRoot("java/com/google/android/apps/example/lib2/SharedActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/lib2/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/lib2/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.libraries.example.lib2")
+                            .setResourceJar(LibraryArtifact.builder()
+                                              .setJar(genRoot("java/com/google/android/apps/example/lib2/lib2_resources.jar"))
+                                              .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib2/lib2_resources.jar"))))
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/lib2/BUILD"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/lib2/lib2.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib2/lib2.jar")))
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/lib2/lib2_resources.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/lib2/lib2_resources.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setKind("android_binary")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .addDependency("//java/com/google/android/apps/example/lib0:lib0")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/example_debug.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/example_debug.jar")))));
+
+    BlazeAndroidImportResult result = importWorkspace(
+      workspaceRoot,
+      response,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.androidResourceModules).containsExactly(
+      AndroidResourceModule.builder(new Label("//java/com/google/android/apps/example:example_debug"))
+        .addResourceAndTransitiveResource(sourceRoot("java/com/google/android/apps/example/res"))
+        .addTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib1/res"))
+        .addTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib2/res"))
+        .addTransitiveResourceDependency("//java/com/google/android/apps/example/lib0:lib0")
+        .addTransitiveResourceDependency("//java/com/google/android/apps/example/lib1:lib1")
+        .addTransitiveResourceDependency("//java/com/google/android/apps/example/lib2:lib2")
+        .build(),
+      AndroidResourceModule.builder(new Label("//java/com/google/android/apps/example/lib1:lib1"))
+        .addResourceAndTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib1/res"))
+        .build(),
+      AndroidResourceModule.builder(new Label("//java/com/google/android/apps/example/lib2:lib2"))
+        .addResourceAndTransitiveResource(sourceRoot("java/com/google/android/apps/example/lib2/res"))
+        .build()
+    );
+
+    assertEquals(1, result.libraries.size());
+    BlazeLibrary library = result.libraries.iterator().next();
+    assertEquals("lib0_resources.jar", library.getLibraryArtifact().jar.getFile().getName());
+  }
+
+  @Test
+  public void testIdlClassJarIsAddedAsLibrary() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("example"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//example:lib")
+          .setBuildFile(sourceRoot("example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("example/MainActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setResourceJavaPackage("example")
+                            .setIdlJar(LibraryArtifact.builder()
+                                         .setJar(genRoot("example/libidl.jar"))
+                                         .setSourceJar(genRoot("example/libidl.srcjar"))
+                                         .build())
+                            .setHasIdlSources(true)));
+
+    BlazeAndroidImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertEquals(1, result.libraries.size());
+
+    BlazeLibrary library = result.libraries.iterator().next();
+    assertEquals("libidl.jar", library.getLibraryArtifact().jar.getFile().getName());
+    assertEquals("libidl.srcjar", library.getLibraryArtifact().sourceJar.getFile().getName());
+  }
+
+  @Test
+  public void testAndroidResourceImport() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/example"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/example:lib")
+          .setBuildFile(sourceRoot("java/com/google/android/example/BUILD"))
+          .setKind("android_library")
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setLegacyResources(new Label("//java/com/google/android/example:resources"))
+                            .setManifestFile(sourceRoot("java/com/google/android/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example"))
+        .build())
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/example:resources")
+          .setBuildFile(sourceRoot("java/com/google/android/example/BUILD"))
+          .setKind("android_resources")
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example"))
+          .build());
+
+    BlazeAndroidImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+    assertThat(result.androidResourceModules).containsExactly(
+      AndroidResourceModule.builder(new Label("//java/com/google/android/example:resources"))
+        .addResourceAndTransitiveResource(sourceRoot("java/com/google/android/example/res"))
+        .build()
+    );
+  }
+
+  @Test
+  public void testResourceImportOutsideSourceFilterIsAddedToResourceLibrary() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/example"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/example:lib")
+          .setBuildFile(sourceRoot("java/com/google/android/example/BUILD"))
+          .setKind("android_library")
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example"))
+          .addDependency("//java/com/google/android/example2:resources")
+          .build())
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/example2:resources")
+          .setBuildFile(sourceRoot("java/com/google/android/example2/BUILD"))
+          .setKind("android_library")
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/example2/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/example2/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example2"))
+          .build());
+
+    BlazeAndroidImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+    BlazeLibrary library = result.libraries.stream()
+      .filter(item -> item.getKey().equals(LibraryKey.forResourceLibrary()))
+      .findAny()
+      .orElse(null);
+    assertThat(library).isNotNull();
+    assertThat(library.getSources()).containsExactly(
+      new File("/root/java/com/google/android/example2/res")
+    );
+  }
+
+  @Test
+  public void testConflictingResourceRClasses() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/example"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/example:lib")
+          .setBuildFile(sourceRoot("java/com/google/android/example/BUILD"))
+          .setKind("android_library")
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example"))
+          .addDependency("//java/com/google/android/example2:resources")
+          .build())
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/example:lib2")
+          .setBuildFile(sourceRoot("java/com/google/android/example2/BUILD"))
+          .setKind("android_library")
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/example2/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/example/res2"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.example"))
+          .build());
+
+    BlazeAndroidImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertIssueContaining("Multiple R classes generated");
+
+    assertThat(result.androidResourceModules).containsExactly(
+      AndroidResourceModule.builder(new Label("//java/com/google/android/example:lib"))
+        .addResourceAndTransitiveResource(sourceRoot("java/com/google/android/example/res"))
+      .build()
+    );
+  }
+
+  private ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder()
+      .setRootPath(FAKE_ROOT)
+      .setRelativePath(relativePath)
+      .setIsSource(true)
+      .build();
+  }
+
+  private static ArtifactLocation genRoot(String relativePath) {
+    return ArtifactLocation.builder()
+      .setRootPath(FAKE_GEN_ROOT)
+      .setRootExecutionPathFragment(FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT)
+      .setRelativePath(relativePath)
+      .setIsSource(false)
+      .build();
+  }
+}
diff --git a/blaze-base/BUILD b/blaze-base/BUILD
new file mode 100644
index 0000000..2473778
--- /dev/null
+++ b/blaze-base/BUILD
@@ -0,0 +1,85 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "blaze-base",
+    srcs = glob(["src/**/*.java"]),
+    resources = glob(["resources/**/*"]),
+    deps = [
+        ":proto-deps",
+        "//intellij-platform-sdk:plugin_api",
+        "//third_party:jsr305",
+        "//third_party:trickle",
+    ],
+)
+
+java_import(
+    name = "proto-deps",
+    jars = ["lib/proto_deps.jar"],
+)
+
+filegroup(
+    name = "plugin_xml",
+    srcs = ["src/META-INF/blaze-base.xml"],
+)
+
+java_library(
+    name = "unit_test_utils",
+    srcs = glob(["tests/utils/unit/**/*.java"]),
+    deps = [
+        ":blaze-base",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
+
+java_library(
+    name = "integration_test_utils",
+    srcs = glob(["tests/utils/integration/**/*.java"]),
+    deps = [
+        ":blaze-base",
+        ":proto-deps",
+        ":unit_test_utils",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
+
+load(
+    "//intellij_test:test_defs.bzl",
+    "intellij_test",
+)
+
+intellij_test(
+    name = "unit_tests",
+    srcs = glob(["tests/unittests/**/*.java"]),
+    test_package_root = "com.google.idea.blaze.base",
+    deps = [
+        ":blaze-base",
+        ":proto-deps",
+        ":unit_test_utils",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//intellij_test:lib",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
+
+intellij_test(
+    name = "integration_tests",
+    srcs = glob(["tests/integrationtests/**/*.java"]),
+    integration_tests = True,
+    required_plugins = "com.google.idea.blaze.ijwb",
+    test_package_root = "com.google.idea.blaze.base",
+    deps = [
+        ":blaze-base",
+        ":integration_test_utils",
+        ":unit_test_utils",
+        "//ijwb:ijwb_bazel",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//intellij_test:lib",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
diff --git a/blaze-base/lib/proto_deps.jar b/blaze-base/lib/proto_deps.jar
new file mode 100755
index 0000000..d9241ab
--- /dev/null
+++ b/blaze-base/lib/proto_deps.jar
Binary files differ
diff --git a/blaze-base/resources/binaries/README b/blaze-base/resources/binaries/README
new file mode 100644
index 0000000..fdc410c
--- /dev/null
+++ b/blaze-base/resources/binaries/README
@@ -0,0 +1,6 @@
+bazel-buildifier
+
+This is a binary built from "https://github.com/bazelbuild/buildifier", providing
+a standardized formatting of BUILD files.
+
+To update, follow the instructions in the github repository.
\ No newline at end of file
diff --git a/blaze-base/resources/binaries/bazel-buildifier b/blaze-base/resources/binaries/bazel-buildifier
new file mode 100755
index 0000000..c7347bb
--- /dev/null
+++ b/blaze-base/resources/binaries/bazel-buildifier
Binary files differ
diff --git a/blaze-base/resources/icons/bazel_leaf.png b/blaze-base/resources/icons/bazel_leaf.png
new file mode 100644
index 0000000..6dfe737
--- /dev/null
+++ b/blaze-base/resources/icons/bazel_leaf.png
Binary files differ
diff --git a/blaze-base/resources/icons/blaze.png b/blaze-base/resources/icons/blaze.png
new file mode 100644
index 0000000..28a66ca
--- /dev/null
+++ b/blaze-base/resources/icons/blaze.png
Binary files differ
diff --git a/blaze-base/resources/icons/blazeToolWindow.png b/blaze-base/resources/icons/blazeToolWindow.png
new file mode 100644
index 0000000..b0f2b69
--- /dev/null
+++ b/blaze-base/resources/icons/blazeToolWindow.png
Binary files differ
diff --git a/blaze-base/resources/icons/blaze_clean.png b/blaze-base/resources/icons/blaze_clean.png
new file mode 100644
index 0000000..1cfe357
--- /dev/null
+++ b/blaze-base/resources/icons/blaze_clean.png
Binary files differ
diff --git a/blaze-base/resources/icons/blaze_dirty.png b/blaze-base/resources/icons/blaze_dirty.png
new file mode 100644
index 0000000..8135e93
--- /dev/null
+++ b/blaze-base/resources/icons/blaze_dirty.png
Binary files differ
diff --git a/blaze-base/resources/icons/blaze_failed.png b/blaze-base/resources/icons/blaze_failed.png
new file mode 100644
index 0000000..7299889
--- /dev/null
+++ b/blaze-base/resources/icons/blaze_failed.png
Binary files differ
diff --git a/blaze-base/resources/icons/blaze_slow.png b/blaze-base/resources/icons/blaze_slow.png
new file mode 100644
index 0000000..7ef6aaa
--- /dev/null
+++ b/blaze-base/resources/icons/blaze_slow.png
Binary files differ
diff --git a/blaze-base/resources/icons/build_editor.png b/blaze-base/resources/icons/build_editor.png
new file mode 100644
index 0000000..9637c4b
--- /dev/null
+++ b/blaze-base/resources/icons/build_editor.png
Binary files differ
diff --git a/blaze-base/resources/icons/build_file.png b/blaze-base/resources/icons/build_file.png
new file mode 100644
index 0000000..9637c4b
--- /dev/null
+++ b/blaze-base/resources/icons/build_file.png
Binary files differ
diff --git a/blaze-base/resources/icons/build_rule.png b/blaze-base/resources/icons/build_rule.png
new file mode 100644
index 0000000..d5c79e1
--- /dev/null
+++ b/blaze-base/resources/icons/build_rule.png
Binary files differ
diff --git a/blaze-base/scripts/create_bugreport.sh b/blaze-base/scripts/create_bugreport.sh
new file mode 100755
index 0000000..8923bae
--- /dev/null
+++ b/blaze-base/scripts/create_bugreport.sh
@@ -0,0 +1,144 @@
+#!/bin/bash
+
+# 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.
+
+#
+# Use this to create a bug report for IntelliJ plugin bugs.
+#
+# Usage: create_bugreport.sh
+#
+
+# For all jars in specified plugin directory, extracts plugin.xml
+# and copies to the specified output directory.
+# args:
+#   - IJ plugin directory
+#   - output directory
+attach_plugin_xmls() {
+  if [ ! -e $1 ]; then return; fi
+  pushd $1 >/dev/null
+  jars=(*.jar)
+  for jar in ${jars[@]}; do
+    plugin=${jar%.jar}
+    if [ -e $jar ]; then
+      mkdir -p $2
+      unzip -p $jar "META-INF/plugin.xml" > "$2/${plugin}.xml"
+      [ $? -eq 0 ] || { exit 1; }
+    fi
+  done
+  popd >/dev/null
+}
+
+files=""
+tmp_dir=$(mktemp -d)
+output_name=intellij-bug-report-$USER
+tar_dir=$tmp_dir/$output_name
+output_file=${output_name}.tar.gz
+
+mkdir -p tar_dir
+[ $? -eq 0 ] || { exit 1; }
+
+# Attach process information
+process_info_dir=$tar_dir/process_info
+mkdir -p $process_info_dir
+
+pids=$(jps | awk '/[0-9]+ (Main)$/ { print $1 }')
+ts=$(date +%H%M%S)
+if [ -z "$pids" ]; then
+  echo "Warn: Could not find any IntelliJ processes."
+fi
+for pid in $pids
+do
+  stack_file=$process_info_dir/stack_${pid}_${ts}.txt
+  mem_file=$process_info_dir/mem_${pid}_${ts}.txt
+  capacity_file=$process_info_dir/capacity_${pid}_${ts}.txt
+  cmd_file=$process_info_dir/cmd_${pid}_${ts}.txt
+  uptime_file=$process_info_dir/uptime_${pid}_${ts}.txt
+  jps -v | grep $pid > $cmd_file
+  jstack $pid > $stack_file
+  jstat -gc $pid > $mem_file
+  jstat -gccapacity $pid > $capacity_file
+  ps -p $pid -o etime= > $uptime_file
+done
+
+# Copy core dumps
+mkdir -p $tar_dir/jvm-dumps
+cp -p $HOME/java_error_in_* $tar_dir/jvm-dumps 2>/dev/null
+
+# Copy vmoptions files
+mkdir -p $tar_dir
+cp -p $HOME/*.vmoptions $tar_dir 2>/dev/null
+
+# Copy details from IJ log directories
+dir_names=(
+  '.IntelliJIdea*'
+  '.IdeaIC*'
+  '.AndroidStudio*'
+  '.CLion*'
+)
+# other product codes, to add if we end up supporting them:
+# PhpStorm, WebStorm, RubyMine, PyCharm, WebIde, AppCode, DataGrip
+
+pushd $HOME >/dev/null
+for log_dirs in ${dir_names[@]}; do
+  for log_dir in $log_dirs ; do
+    if [ ! -e $log_dir ]; then
+      continue
+    fi
+    product=${log_dir:1}
+    mkdir ${tar_dir}/${product}
+    pushd ${tar_dir}/${product} >/dev/null
+
+    # Attach user's log directories
+    if [ -d ${HOME}/${log_dir}/system/log ]; then
+      mkdir -p "system/log"
+      cp -r "${HOME}/${log_dir}/system/log" "system"
+      [ $? -eq 0 ] || { exit 1; }
+    fi
+
+    # Attach product version
+    ij_home=$(<"${HOME}/${log_dir}/system/.home")
+    cp "${ij_home}/build.txt" "version"
+
+    # Attach plugin.xmls
+    attach_plugin_xmls "${HOME}/${log_dir}/system/plugins" "${tar_dir}/${product}/system/plugins"
+    attach_plugin_xmls "${HOME}/${log_dir}/config/plugins" "${tar_dir}/${product}/config/plugins"
+
+    # copy vmoptions
+    mkdir -p vmoptions/home
+    cp -p ${HOME}/${log_dir}/*.vmoptions vmoptions/home 2>/dev/null
+    mkdir -p vmoptions/installation
+    cp -p ${ij_home}/bin/*.vmoptions vmoptions/installation 2>/dev/null
+
+    popd >/dev/null
+  done
+done
+popd >/dev/null
+
+# Attach user's .blazerc
+if [ -f $HOME/.blazerc ]; then
+  cp $HOME/.blazerc $tar_dir/.blazerc
+fi
+
+pushd $tmp_dir >/dev/null
+tar -chzf $output_file $output_name
+[ $? -eq 0 ] || { exit 1; }
+popd >/dev/null
+
+tar_file=$tmp_dir/$output_file
+echo "Bug report produced in: $tar_file"
+echo "Path has been copied to clipboard."
+echo -n $tar_file | xclip -selection primary
+echo -n $tar_file | xclip -selection clipboard
+
diff --git a/blaze-base/src/META-INF/blaze-base.xml b/blaze-base/src/META-INF/blaze-base.xml
new file mode 100644
index 0000000..4668d85
--- /dev/null
+++ b/blaze-base/src/META-INF/blaze-base.xml
@@ -0,0 +1,262 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+  <actions>
+    <action id="MakeBlazeProject" class="com.google.idea.blaze.base.actions.BlazeMakeProjectAction" use-shortcut-of="CompileDirty" icon="AllIcons.Actions.Compile">
+    </action>
+    <action id="MakeBlazeModule" class="com.google.idea.blaze.base.actions.BlazeCompileFileAction">
+    </action>
+    <action id="Blaze.IncrementalSyncProject" class="com.google.idea.blaze.base.sync.actions.IncrementalSyncProjectAction" icon="BlazeIcons.Blaze">
+    </action>
+    <action id="Blaze.FullSyncProject" class="com.google.idea.blaze.base.sync.actions.FullSyncProjectAction" icon="BlazeIcons.BlazeSlow">
+    </action>
+    <action id="Blaze.ExpandSyncToWorkingSet" class="com.google.idea.blaze.base.sync.actions.ExpandSyncToWorkingSetAction" text="Expand Sync to Working Set">
+    </action>
+    <action id="Blaze.ShowPerformanceWarnings" class="com.google.idea.blaze.base.sync.actions.ShowPerformanceWarningsToggleAction" text="Show Performance Warnings">
+    </action>
+    <action id="Blaze.EditProjectView" class="com.google.idea.blaze.base.settings.ui.EditProjectViewAction" text="Edit Project View..." icon="BlazeIcons.Blaze">
+    </action>
+
+    <action class="com.google.idea.blaze.base.buildmap.OpenCorrespondingBuildFile"
+            id="Blaze.OpenCorrespondingBuildFile"
+            icon="BlazeIcons.Blaze"
+            text="Open Corresponding BUILD File">
+    </action>
+    <action class="com.google.idea.blaze.base.sync.actions.PartialSyncAction"
+            id="Blaze.PartialSync"
+            icon="BlazeIcons.Blaze">
+    </action>
+
+    <group id="Blaze.MainMenuActionGroup" class="com.google.idea.blaze.base.actions.BlazeMenuGroup">
+      <add-to-group group-id="MainMenu" anchor="before" relative-to-action="HelpMenu"/>
+      <reference id="MakeBlazeProject"/>
+      <reference id="MakeBlazeModule"/>
+      <separator/>
+      <reference id="Blaze.EditProjectView"/>
+      <separator/>
+      <reference id="Blaze.IncrementalSyncProject"/>
+      <reference id="Blaze.FullSyncProject"/>
+      <reference id="Blaze.PartialSync"/>
+      <reference id="Blaze.ExpandSyncToWorkingSet"/>
+      <reference id="Blaze.ShowPerformanceWarnings"/>
+    </group>
+
+    <group id="Blaze.MainToolBarActionGroup">
+      <add-to-group group-id="MainToolBar" anchor="before" relative-to-action="HelpTopics" />
+      <add-to-group group-id="NavBarToolBarOthers" anchor="last"/>
+      <reference id="Blaze.IncrementalSyncProject"/>
+    </group>
+
+    <group id="Blaze.NewActions" text="Edit Blaze structure" description="Create new Blaze packages, rules, etc.">
+      <add-to-group group-id="NewGroup" anchor="first"/>
+      <action id="Blaze.NewPackageAction" class="com.google.idea.blaze.base.ide.NewBlazePackageAction" popup="true"/>
+      <action id="Blaze.NewRuleAction" class="com.google.idea.blaze.base.ide.NewBlazeRuleAction" popup="true"/>
+      <separator/>
+    </group>
+
+    <group id="Blaze.ProjectViewPopupMenu">
+      <add-to-group anchor="after" group-id="ProjectViewPopupMenu" relative-to-action="EditSource"/>
+      <separator/>
+      <reference ref="Blaze.PartialSync"/>
+      <reference ref="Blaze.OpenCorrespondingBuildFile"/>
+    </group>
+
+    <group id="Blaze.EditorTabPopupMenu">
+      <add-to-group anchor="after" group-id="EditorTabPopupMenu" relative-to-action="CopyReference"/>
+      <separator/>
+      <reference ref="Blaze.PartialSync"/>
+      <reference ref="Blaze.OpenCorrespondingBuildFile"/>
+    </group>
+  </actions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <postStartupActivity implementation="com.google.idea.blaze.base.sync.BlazeSyncStartupActivity"/>
+
+    <toolWindow id="Blaze Console"
+                      anchor="bottom"
+                      secondary="true"
+                      conditionClass="com.google.idea.blaze.base.settings.IsBlazeProjectCondition"
+                      icon="BlazeIcons.BlazeToolWindow"
+                      factoryClass="com.google.idea.blaze.base.console.BlazeConsoleToolWindowFactory"/>
+    <projectService serviceImplementation="com.google.idea.blaze.base.console.BlazeConsoleView"/>
+    <fileTypeFactory implementation="com.google.idea.blaze.base.plugin.BlazeFileTypeFactory" />
+    <applicationService serviceInterface="com.google.idea.blaze.base.experiments.ExperimentService"
+                        serviceImplementation="com.google.idea.blaze.base.experiments.ExperimentServiceImpl"/>
+
+    <projectConfigurable instance="com.google.idea.blaze.base.settings.ui.BlazeUserSettingsConfigurable"
+                         id ="blaze.view" displayName="Blaze settings"/>
+
+    <projectService serviceInterface="com.google.idea.blaze.base.sync.data.BlazeProjectDataManager"
+                    serviceImplementation="com.google.idea.blaze.base.sync.data.BlazeProjectDataManagerImpl"/>
+    <projectService serviceImplementation="com.google.idea.blaze.base.sync.BlazeSyncManager"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.sync.status.BlazeSyncStatus"
+                    serviceImplementation="com.google.idea.blaze.base.sync.status.BlazeSyncStatusImpl"/>
+
+    <applicationService serviceInterface="com.google.idea.blaze.base.async.executor.BlazeExecutor"
+                        serviceImplementation="com.google.idea.blaze.base.async.executor.BlazeExecutorImpl"/>
+    <projectService serviceInterface="com.intellij.openapi.vcs.impl.DefaultVcsRootPolicy"
+                    serviceImplementation="com.google.idea.blaze.base.vcs.BlazeDefaultVcsRootPolicy"
+                    overrides="true"/>
+    <fileDocumentManagerListener implementation="com.google.idea.blaze.base.buildmodifier.FileSaveHandler" order="first"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.io.InputStreamProvider"
+                        serviceImplementation="com.google.idea.blaze.base.io.InputStreamProviderImpl"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.io.FileAttributeProvider"
+                        serviceImplementation="com.google.idea.blaze.base.io.FileAttributeProvider"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.io.WorkspaceScanner"
+                        serviceImplementation="com.google.idea.blaze.base.io.VfsWorkspaceScanner"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.buildmodifier.BuildFileModifier"
+                        serviceImplementation="com.google.idea.blaze.base.lang.buildfile.actions.BuildFileModifierImpl"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.buildmodifier.FileSystemModifier"
+                    serviceImplementation="com.google.idea.blaze.base.buildmodifier.FileSystemModifierImpl"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.run.rulefinder.RuleFinder"
+                        serviceImplementation="com.google.idea.blaze.base.run.rulefinder.RuleFinderImpl"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.command.info.BlazeInfo"
+                        serviceImplementation="com.google.idea.blaze.base.command.info.BlazeInfoImpl"/>
+
+    <treeStructureProvider implementation="com.google.idea.blaze.base.treeview.BlazeTreeStructureProvider" id="blaze"/>
+
+    <applicationService serviceInterface="com.google.idea.blaze.base.projectview.ProjectViewStorageManager"
+                        serviceImplementation="com.google.idea.blaze.base.projectview.ProjectViewStorageManagerImpl"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.projectview.ProjectViewManager"
+                    serviceImplementation="com.google.idea.blaze.base.projectview.ProjectViewManagerImpl"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface"
+                        serviceImplementation="com.google.idea.blaze.base.sync.aspects.BlazeIdeInterfaceAspectsImpl"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.run.TestRuleFinder"
+                        serviceImplementation="com.google.idea.blaze.base.run.testmap.TestRuleFinderImpl"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.console.BlazeConsoleService"
+                    serviceImplementation="com.google.idea.blaze.base.console.BlazeConsoleServiceImpl"/>
+    <projectService serviceImplementation="com.google.idea.blaze.base.buildmap.FileToBuildMap"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.rulemaps.SourceToRuleMap"
+                    serviceImplementation="com.google.idea.blaze.base.rulemaps.SourceToRuleMapImpl"/>
+    <projectService serviceImplementation="com.google.idea.blaze.base.settings.BlazeImportSettingsManager"/>
+    <applicationService serviceImplementation="com.google.idea.blaze.base.settings.BlazeUserSettings"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider"
+                        serviceImplementation="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProviderImpl"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider"
+                        serviceImplementation="com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProviderImpl"/>
+    <applicationService serviceInterface="com.google.idea.blaze.base.prefetch.PrefetchService"
+                        serviceImplementation="com.google.idea.blaze.base.prefetch.PrefetchServiceImpl"/>
+    <applicationService serviceImplementation="com.google.idea.blaze.base.wizard2.BlazeWizardUserSettingsStorage"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverProvider"
+                    serviceImplementation="com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverProviderImpl"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <fileTypeFactory implementation="com.google.idea.blaze.base.lang.projectview.language.ProjectViewFileTypeFactory"/>
+    <lang.parserDefinition language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.parser.ProjectViewParserDefinition"/>
+    <lang.commenter language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.formatting.ProjectViewCommenter"/>
+    <lang.syntaxHighlighterFactory language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.highlighting.ProjectViewSyntaxHighlighterFactory"/>
+    <completion.contributor language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.completion.ProjectViewKeywordCompletionContributor"/>
+    <completion.contributor language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.completion.WorkspaceTypeCompletionContributor"/>
+    <completion.contributor language="projectview" implementationClass="com.google.idea.blaze.base.lang.projectview.completion.AdditionalLanguagesCompletionContributor"/>
+    <enterHandlerDelegate implementation="com.google.idea.blaze.base.lang.projectview.formatting.ProjectViewEnterHandler"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <fileTypeFactory implementation="com.google.idea.blaze.base.lang.buildfile.language.BuildFileTypeFactory"/>
+    <annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.HighlightingAnnotator"/>
+    <!--<annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.ErrorAnnotator"/>-->
+    <annotator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.validation.GlobErrorAnnotator"/>
+    <colorSettingsPage implementation="com.google.idea.blaze.base.lang.buildfile.highlighting.BuildColorsPage"/>
+    <projectService serviceImplementation="com.google.idea.blaze.base.lang.buildfile.psi.util.BuildElementGenerator"/>
+    <projectService serviceImplementation="com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager"/>
+    <referencesSearch implementation="com.google.idea.blaze.base.lang.buildfile.search.BuildLabelReferenceSearcher"/>
+    <referencesSearch implementation="com.google.idea.blaze.base.lang.buildfile.search.GlobReferenceSearcher"/>
+    <readWriteAccessDetector implementation="com.google.idea.blaze.base.lang.buildfile.findusages.BuildReadWriteAccessDetector"/>
+    <elementDescriptionProvider implementation="com.google.idea.blaze.base.lang.buildfile.findusages.BuildElementDescriptionProvider"/>
+    <usageGroupingRuleProvider implementation="com.google.idea.blaze.base.lang.buildfile.findusages.BuildUsageGroupingRuleProvider"/>
+    <useScopeOptimizer implementation="com.google.idea.blaze.base.lang.buildfile.search.ExcludeBuildFilesScope"/>
+    <targetElementEvaluator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.findusages.BuildTargetElementEvaluator"/>
+    <quoteHandler fileType="BUILD" className="com.google.idea.blaze.base.lang.buildfile.editor.BuildQuoteHandler"/>
+    <enterHandlerDelegate implementation="com.google.idea.blaze.base.lang.buildfile.editor.BuildEnterBetweenBracketsHandler" order="before EnterBetweenBracesHandler"/>
+    <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.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"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.intellij.lang">
+    <syntaxHighlighterFactory language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.highlighting.BuildSyntaxHighlighterFactory"/>
+    <parserDefinition language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.parser.BuildParserDefinition"/>
+    <namesValidator language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.refactor.BuildNamesValidator"/>
+    <braceMatcher language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.formatting.BuildBraceMatcher"/>
+    <commenter language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.formatting.BuildCommenter"/>
+    <foldingBuilder language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.formatting.BuildFileFoldingBuilder"/>
+    <psiStructureViewFactory language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.views.BuildStructureViewFactory"/>
+    <findUsagesProvider language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.findusages.BuildFindUsagesProvider"/>
+    <refactoringSupport language="BUILD" implementationClass="com.google.idea.blaze.base.lang.buildfile.refactor.BuildRefactoringSupportProvider"/>
+  </extensions>
+
+  <extensionPoints>
+    <extensionPoint qualifiedName="com.google.idea.blaze.base.lang.buildfile.DumbAnnotator" interface="com.google.idea.blaze.base.lang.buildfile.validation.BuildAnnotator"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.base.lang.buildfile.Annotator" interface="com.google.idea.blaze.base.lang.buildfile.validation.BuildAnnotator"/>
+  </extensionPoints>
+
+  <application-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.base.plugin.BlazeSpecificInitializer</implementation-class>
+    </component>
+    <component>
+      <implementation-class>com.google.idea.blaze.base.plugin.dependency.ProjectDependencyMigration</implementation-class>
+    </component>
+  </application-components>
+
+  <project-components>
+    <component>
+      <implementation-class>com.google.idea.blaze.base.prefetch.PrefetchServiceProjectComponent</implementation-class>
+      <skipForDefaultProject/>
+    </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"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.RuleConfigurationFactory" interface="com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.Prefetcher"
+                    interface="com.google.idea.blaze.base.prefetch.Prefetcher"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.PsiFileProvider" interface="com.google.idea.blaze.base.lang.buildfile.search.PsiFileProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.VcsHandler"
+                    interface="com.google.idea.blaze.base.vcs.BlazeVcsHandler"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BlazeWizardOptionProvider"
+                    interface="com.google.idea.blaze.base.wizard2.BlazeWizardOptionProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.DefaultSdkProvider"
+                    interface="com.google.idea.blaze.base.sync.sdk.DefaultSdkProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BuildFlagsProvider" interface="com.google.idea.blaze.base.command.BuildFlagsProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BuildSystemProvider" interface="com.google.idea.blaze.base.bazel.BuildSystemProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.BuildifierBinaryProvider" interface="com.google.idea.blaze.base.buildmodifier.BuildifierBinaryProvider"/>
+    <extensionPoint qualifiedName="com.google.idea.blaze.LoggingService" interface="com.google.idea.blaze.base.metrics.LoggingService"/>
+  </extensionPoints>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncListener implementation="com.google.idea.blaze.base.run.BlazeRunConfigurationSyncListener"/>
+    <SyncListener implementation="com.google.idea.blaze.base.sync.status.BlazeSyncStatusListener"/>
+    <SyncListener implementation="com.google.idea.blaze.base.run.testmap.TestRuleFinderImpl$ClearTestMap"/>
+    <SyncListener implementation="com.google.idea.blaze.base.rulemaps.SourceToRuleMapImpl$ClearSourceToTargetMap"/>
+    <SyncListener implementation="com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProviderImpl"/>
+    <SyncPlugin implementation="com.google.idea.blaze.base.lang.buildfile.sync.BuildLangSyncPlugin"/>
+    <BlazeWizardOptionProvider implementation="com.google.idea.blaze.base.wizard2.BazelWizardOptionProvider"/>
+    <BuildFlagsProvider implementation="com.google.idea.blaze.base.command.BuildFlagsProviderImpl"/>
+    <VcsHandler implementation="com.google.idea.blaze.base.vcs.git.GitBlazeVcsHandler"/>
+    <VcsHandler implementation="com.google.idea.blaze.base.vcs.FallbackBlazeVcsHandler" order="last" id="fallback"/>
+    <BuildSystemProvider implementation="com.google.idea.blaze.base.bazel.BazelBuildSystemProvider" order="last"/>
+    <BuildifierBinaryProvider implementation="com.google.idea.blaze.base.buildmodifier.BazelBuildifierBinaryProvider"/>
+  </extensions>
+
+</idea-plugin>
diff --git a/blaze-base/src/com/google/idea/blaze/base/actions/BlazeAction.java b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeAction.java
new file mode 100644
index 0000000..47babbc
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeAction.java
@@ -0,0 +1,64 @@
+/*
+ * 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.actions;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * Base class action that hides for non-blaze projects.
+ */
+public abstract class BlazeAction extends AnAction {
+  protected BlazeAction() {
+  }
+
+  protected BlazeAction(Icon icon) {
+    super(icon);
+  }
+
+  protected BlazeAction(@Nullable String text) {
+    super(text);
+  }
+
+  protected BlazeAction(@Nullable String text, @Nullable String description, @Nullable Icon icon) {
+    super(text, description, icon);
+  }
+
+  @Override
+  public final void update(AnActionEvent e) {
+    if (!isBlazeProject(e)) {
+      e.getPresentation().setEnabledAndVisible(false);
+      return;
+    }
+
+    e.getPresentation().setEnabledAndVisible(true);
+    doUpdate(e);
+  }
+
+  protected void doUpdate(@NotNull AnActionEvent e) {
+  }
+
+  private static boolean isBlazeProject(@NotNull AnActionEvent e) {
+    Project project = e.getProject();
+    return project != null && Blaze.isBlazeProject(project);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java
new file mode 100644
index 0000000..13dbaa8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeCompileFileAction.java
@@ -0,0 +1,125 @@
+/*
+ * 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.actions;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.experiments.ExperimentScope;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.scopes.*;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.List;
+
+public class BlazeCompileFileAction extends BlazeAction {
+  private static final Logger LOG = Logger.getInstance(BlazeCompileFileAction.class);
+
+  public BlazeCompileFileAction() {
+    super("Compile file");
+  }
+
+  @Override
+  protected void doUpdate(@NotNull AnActionEvent e) {
+    // IntelliJ uses different logic for 1 vs many module selection. When many modules are selected
+    // modules with more than 1 content root are ignored
+    // (ProjectViewImpl#moduleBySingleContentRoot).
+    if (getTargets(e).isEmpty()) {
+      Presentation presentation = e.getPresentation();
+      presentation.setEnabled(false);
+    }
+  }
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project != null) {
+      ImmutableCollection<Label> targets = getTargets(e);
+      buildSourceFile(project, targets);
+    }
+  }
+
+  private ImmutableCollection<Label> getTargets(AnActionEvent e) {
+    Project project = e.getProject();
+    VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE);
+    if (project != null && virtualFile != null) {
+      return SourceToRuleMap.getInstance(project).getTargetsForSourceFile(new File(virtualFile.getPath()));
+    }
+    return ImmutableList.of();
+  }
+
+  private static void buildSourceFile(
+    @NotNull Project project,
+    @NotNull ImmutableCollection<Label> targets) {
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null || targets.isEmpty()) {
+      return;
+    }
+    final ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet == null) {
+      return;
+    }
+    BlazeExecutor.submitTask(project, new ScopedTask() {
+      @Override
+      public void execute(@NotNull BlazeContext context) {
+        context
+          .push(new ExperimentScope())
+          .push(new BlazeConsoleScope.Builder(project).build())
+          .push(new IssuesScope(project))
+          .push(new TimingScope("Make"))
+          .push(new LoggedTimingScope(project, Action.MAKE_MODULE_TOTAL_TIME))
+          .push(new NotificationScope(
+            project,
+            "Make",
+            "Make module",
+            "Make module completed successfully",
+            "Make module failed"
+          ))
+        ;
+
+        WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+
+        SaveUtil.saveAllFiles();
+        BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
+
+        List<TargetExpression> targetExpressions = Lists.newArrayList(targets);
+        blazeIdeInterface.resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, targetExpressions);
+        LocalFileSystem.getInstance().refresh(true);
+      }
+    });
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java
new file mode 100644
index 0000000..51acab9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeMakeProjectAction.java
@@ -0,0 +1,99 @@
+/*
+ * 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.actions;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.google.idea.blaze.base.experiments.ExperimentScope;
+import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.scopes.*;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class BlazeMakeProjectAction extends BlazeAction {
+
+  public BlazeMakeProjectAction() {
+    super("Make Project");
+  }
+
+  @Override
+  public final void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project != null && Blaze.isBlazeProject(project)) {
+      buildBlazeProject(project);
+    }
+  }
+
+  protected void buildBlazeProject(@NotNull final Project project) {
+
+    BlazeExecutor.submitTask(project, new ScopedTask() {
+      @Override
+      public void execute(@NotNull BlazeContext context) {
+        context
+          .push(new ExperimentScope())
+          .push(new BlazeConsoleScope.Builder(project).build())
+          .push(new IssuesScope(project))
+          .push(new TimingScope("Make"))
+          .push(new LoggedTimingScope(project, Action.MAKE_PROJECT_TOTAL_TIME))
+          .push(new NotificationScope(
+            project,
+            "Make",
+            "Make project",
+            "Make project completed successfully",
+            "Make project failed"))
+        ;
+
+        BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+        if (blazeProjectData == null) {
+          return;
+        }
+        ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).reloadProjectView(
+          context,
+          blazeProjectData.workspacePathResolver
+        );
+        if (projectViewSet == null) {
+          return;
+        }
+
+        WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+
+        List<TargetExpression> targets = Lists.newArrayList();
+        targets.addAll(projectViewSet.listItems(TargetSection.KEY));
+
+        SaveUtil.saveAllFiles();
+        BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
+        blazeIdeInterface.resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, targets);
+        LocalFileSystem.getInstance().refresh(true);
+      }
+    });
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/actions/BlazeMenuGroup.java b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeMenuGroup.java
new file mode 100644
index 0000000..36b45c9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeMenuGroup.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.actions;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+public class BlazeMenuGroup extends DefaultActionGroup {
+  @Override
+  public final void update(AnActionEvent e) {
+    if (!isBlazeProject(e)) {
+      e.getPresentation().setEnabledAndVisible(false);
+      return;
+    }
+
+    e.getPresentation().setEnabledAndVisible(true);
+    e.getPresentation().setText(Blaze.buildSystemName(e.getProject()));
+  }
+
+  @Override
+  public boolean isDumbAware() {
+    return true;
+  }
+
+  private static boolean isBlazeProject(@NotNull AnActionEvent e) {
+    Project project = e.getProject();
+    return project != null && Blaze.isBlazeProject(project);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/actions/BlazeToggleAction.java b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeToggleAction.java
new file mode 100644
index 0000000..9480fec
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/actions/BlazeToggleAction.java
@@ -0,0 +1,61 @@
+/*
+ * 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.actions;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.ToggleAction;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * Base class toggle action that hides for non-blaze projects.
+ */
+public abstract class BlazeToggleAction extends ToggleAction {
+  protected BlazeToggleAction() {
+  }
+
+  protected BlazeToggleAction(@Nullable String text) {
+    super(text);
+  }
+
+  protected BlazeToggleAction(@Nullable String text, @Nullable String description, @Nullable Icon icon) {
+    super(text, description, icon);
+  }
+
+  @Override
+  public final void update(AnActionEvent e) {
+    if (!isBlazeProject(e)) {
+      e.getPresentation().setEnabledAndVisible(false);
+      return;
+    }
+
+    e.getPresentation().setEnabledAndVisible(true);
+    super.update(e);
+    doUpdate(e);
+  }
+
+  protected void doUpdate(@NotNull AnActionEvent e) {
+  }
+
+  private static boolean isBlazeProject(@NotNull AnActionEvent e) {
+    Project project = e.getProject();
+    return project != null && Blaze.isBlazeProject(project);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/AsyncUtil.java b/blaze-base/src/com/google/idea/blaze/base/async/AsyncUtil.java
new file mode 100644
index 0000000..1e8a525
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/AsyncUtil.java
@@ -0,0 +1,58 @@
+/*
+ * 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.async;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Async utilities.
+ */
+public class AsyncUtil {
+  public static void executeProjectChangeAction(@NotNull final Runnable task) throws Throwable {
+    final ValueHolder<Throwable> error = new ValueHolder<Throwable>();
+
+    executeOnEdt(new Runnable() {
+      @Override
+      public void run() {
+        ApplicationManager.getApplication().runWriteAction(new Runnable() {
+          @Override
+          public void run() {
+            try {
+              task.run();
+            } catch (Throwable t) {
+              error.value = t;
+            }
+          }
+        });
+      }
+    });
+
+    if (error.value != null) {
+      throw error.value;
+    }
+  }
+
+  private static void executeOnEdt(@NotNull Runnable task) {
+    if (ApplicationManager.getApplication().isDispatchThread()) {
+      task.run();
+    }
+    else {
+      UIUtil.invokeAndWaitIfNeeded(task);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/FutureUtil.java b/blaze-base/src/com/google/idea/blaze/base/async/FutureUtil.java
new file mode 100644
index 0000000..3c6b9e3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/FutureUtil.java
@@ -0,0 +1,110 @@
+/*
+ * 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.async;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.intellij.openapi.diagnostic.Logger;
+
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Utilities operating on futures.
+ */
+public class FutureUtil {
+  public static class FutureResult<T> {
+    private final T result;
+    private final boolean success;
+
+    FutureResult(T result) {
+      this.result = result;
+      this.success = true;
+    }
+
+    FutureResult() {
+      this.result = null;
+      this.success = false;
+    }
+
+    public T result() {
+      return result;
+    }
+
+    public boolean success() {
+      return success;
+    }
+  }
+
+  public static class Builder<T> {
+    private static final Logger LOG = Logger.getInstance(FutureUtil.class);
+    private final BlazeContext context;
+    private final ListenableFuture<T> future;
+    private String timingCategory;
+    private String errorMessage;
+    private String progressMessage;
+
+    Builder(BlazeContext context, ListenableFuture<T> future) {
+      this.context = context;
+      this.future = future;
+    }
+
+    public Builder<T> timed(String timingCategory) {
+      this.timingCategory = timingCategory;
+      return this;
+    }
+    public Builder<T> withProgressMessage(String message) {
+      this.progressMessage = message;
+      return this;
+    }
+    public Builder<T> onError(String errorMessage) {
+      this.errorMessage = errorMessage;
+      return this;
+    }
+    public FutureResult<T> run() {
+      return Scope.push(context, (childContext) -> {
+        if (timingCategory != null) {
+          childContext.push(new TimingScope(timingCategory));
+        }
+        if (progressMessage != null) {
+          childContext.output(new PrintOutput(progressMessage));
+        }
+        try {
+          return new FutureResult<>(future.get());
+        }
+        catch (InterruptedException e) {
+          Thread.currentThread().interrupt();
+          context.setCancelled();
+        }
+        catch (ExecutionException e) {
+          LOG.error(e);
+          if (errorMessage != null) {
+            IssueOutput.error(errorMessage).submit(childContext);
+          }
+          context.setHasError();
+        }
+        return new FutureResult<>();
+      });
+    }
+  }
+
+  public static <T> Builder<T> waitForFuture(BlazeContext context, ListenableFuture<T> future) {
+    return new Builder<>(context, future);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/ValueHolder.java b/blaze-base/src/com/google/idea/blaze/base/async/ValueHolder.java
new file mode 100644
index 0000000..97f514d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/ValueHolder.java
@@ -0,0 +1,23 @@
+/*
+ * 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.async;
+
+/**
+ * Simple wrapper to work around Java's final limitation.
+ */
+public class ValueHolder<T> {
+  public T value;
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java b/blaze-base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java
new file mode 100644
index 0000000..49d11c0
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/executor/BlazeExecutor.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.async.executor;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.progress.PerformInBackgroundOption;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.progress.Progressive;
+import com.intellij.openapi.progress.impl.BackgroundableProcessIndicator;
+import com.intellij.openapi.progress.util.AbstractProgressIndicatorExBase;
+import com.intellij.openapi.progress.util.ProgressWindow;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Computable;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.concurrent.Callable;
+
+/**
+ * Shared thread pool for blaze tasks.
+ */
+public abstract class BlazeExecutor {
+  public static enum Modality {
+    MODAL, // This task must start in the foreground and stay there.
+    BACKGROUNDABLE, // This task will start in the foreground, but can be sent to the background.
+    ALWAYS_BACKGROUND // This task will start in the background and stay there.
+  }
+
+  @NotNull
+  public static BlazeExecutor getInstance() {
+    return ServiceManager.getService(BlazeExecutor.class);
+  }
+
+  public abstract <T> ListenableFuture<T> submit(Callable<T> callable);
+
+  public abstract ListeningExecutorService getExecutor();
+
+  public static ListenableFuture<Void> submitTask(
+    @Nullable final Project project,
+    @NotNull final Progressive progressive) {
+    return submitTask(project, "", progressive);
+  }
+
+  public static ListenableFuture<Void> submitTask(
+    @Nullable final Project project,
+    @NotNull final String title,
+    @NotNull final Progressive progressive) {
+    return submitTask(
+      project,
+      title,
+      true /* cancelable */,
+      Modality.ALWAYS_BACKGROUND,
+      progressive);
+  }
+
+  public static ListenableFuture<Void> submitTask(
+    @Nullable final Project project,
+    @NotNull final String title,
+    final boolean cancelable,
+    final Modality modality,
+    @NotNull final Progressive progressive) {
+
+    // The progress indicator must be created on the UI thread.
+    final ProgressWindow indicator = UIUtil.invokeAndWaitIfNeeded(new Computable<ProgressWindow>() {
+      @Override
+      public ProgressWindow compute() {
+        if (modality == Modality.MODAL) {
+          ProgressWindow indicator = new ProgressWindow(cancelable, project);
+          indicator.setTitle(title);
+          return indicator;
+        }
+        else {
+          PerformInBackgroundOption backgroundOption = modality == Modality.BACKGROUNDABLE ?
+                                                       PerformInBackgroundOption.DEAF :
+                                                       PerformInBackgroundOption.ALWAYS_BACKGROUND;
+          return new BackgroundableProcessIndicator(
+            project,
+            title,
+            backgroundOption,
+            "Cancel",
+            "Cancel",
+            cancelable
+          );
+        }
+      }
+    });
+
+    indicator.setIndeterminate(true);
+    indicator.start();
+    final Runnable process = new Runnable() {
+      @Override
+      public void run() {
+        progressive.run(indicator);
+      }
+    };
+    final ListenableFuture<Void> future = getInstance().submit(new Callable<Void>() {
+      @Override
+      public Void call() throws Exception {
+        ProgressManager.getInstance().runProcess(process, indicator);
+        return null;
+      }
+    });
+    if (cancelable) {
+      indicator.addStateDelegate(new AbstractProgressIndicatorExBase() {
+        @Override
+        public void cancel() {
+          super.cancel();
+          future.cancel(true);
+        }
+      });
+    }
+    return future;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/executor/BlazeExecutorImpl.java b/blaze-base/src/com/google/idea/blaze/base/async/executor/BlazeExecutorImpl.java
new file mode 100644
index 0000000..f8407b6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/executor/BlazeExecutorImpl.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.async.executor;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.*;
+
+/**
+ * Executes blaze tasks on the an executor.
+ */
+public class BlazeExecutorImpl extends BlazeExecutor {
+
+  private final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(16));
+
+  @Override
+  public <T> ListenableFuture<T> submit(Callable<T> callable) {
+    return executorService.submit(callable);
+  }
+
+  @Override
+  public ListeningExecutorService getExecutor() {
+    return executorService;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/executor/TransientExecutor.java b/blaze-base/src/com/google/idea/blaze/base/async/executor/TransientExecutor.java
new file mode 100644
index 0000000..f08b0c2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/executor/TransientExecutor.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.async.executor;
+
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An executor that grows to a finite number of threads and times them out quickly.
+ */
+public class TransientExecutor extends ThreadPoolExecutor {
+  public TransientExecutor(int maxThreads) {
+    super(maxThreads, maxThreads, 200, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
+    allowCoreThreadTimeOut(true);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/process/ExternalTask.java b/blaze-base/src/com/google/idea/blaze/base/async/process/ExternalTask.java
new file mode 100644
index 0000000..8fd2de2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/process/ExternalTask.java
@@ -0,0 +1,244 @@
+/*
+ * 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.async.process;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Throwables;
+import com.google.common.io.ByteStreams;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProcessCanceledException;
+import com.intellij.util.SystemProperties;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * Invokes an external process
+ */
+public class ExternalTask {
+  private static final Logger LOG = Logger.getInstance(ExternalTask.class);
+
+  static final OutputStream NULL_STREAM = ByteStreams.nullOutputStream();
+
+  public static class Builder {
+    @NotNull
+    private final File workingDirectory;
+    @NotNull
+    private final List<String> command;
+    @Nullable
+    private BlazeContext context;
+    @Nullable
+    private OutputStream stdout;
+    @Nullable
+    private OutputStream stderr;
+    boolean redirectErrorStream = false;
+
+    private Builder(
+      @NotNull WorkspaceRoot workspaceRoot,
+      @NotNull List<String> command) {
+      this(workspaceRoot.directory(), command);
+    }
+
+    private Builder(
+      @NotNull File workingDirectory,
+      @NotNull List<String> command) {
+      this.workingDirectory = workingDirectory;
+      this.command = command;
+    }
+
+    @NotNull
+    public Builder context(@Nullable BlazeContext context) {
+      this.context = context;
+      return this;
+    }
+
+    @NotNull
+    public Builder redirectStderr(boolean redirectStderr) {
+      this.redirectErrorStream = redirectStderr;
+      return this;
+    }
+
+    @NotNull
+    public Builder stdout(@Nullable OutputStream stdout) {
+      this.stdout = stdout;
+      return this;
+    }
+
+    @NotNull
+    public Builder stderr(@Nullable OutputStream stderr) {
+      this.stderr = stderr;
+      return this;
+    }
+
+    @NotNull
+    public ExternalTask build() {
+      return new ExternalTask(
+        context,
+        workingDirectory,
+        command,
+        stdout,
+        stderr,
+        redirectErrorStream
+      );
+    }
+  }
+
+  @NotNull
+  private final File workingDirectory;
+
+  @NotNull
+  private final List<String> command;
+
+  @Nullable
+  private final BlazeContext parentContext;
+
+  private final boolean redirectErrorStream;
+
+  @NotNull
+  private final OutputStream stdout;
+
+  @NotNull
+  private final OutputStream stderr;
+
+  private ExternalTask(
+    @Nullable BlazeContext context,
+    @NotNull File workingDirectory,
+    @NotNull List<String> command,
+    @Nullable OutputStream stdout,
+    @Nullable OutputStream stderr,
+    boolean redirectErrorStream) {
+    this.workingDirectory = workingDirectory;
+    this.command = command;
+    this.parentContext = context;
+    this.redirectErrorStream = redirectErrorStream;
+    this.stdout = stdout != null ? stdout : NULL_STREAM;
+    this.stderr = stderr != null ? stderr : NULL_STREAM;
+  }
+
+  public int run(BlazeScope... scopes) {
+    Integer returnValue = Scope.push(parentContext, context -> {
+      for (BlazeScope scope : scopes) {
+        context.push(scope);
+      }
+      try {
+        return invokeCommand(context);
+      } catch (ProcessCanceledException e) {
+        // Logging a ProcessCanceledException is an IJ error - mark context canceled instead.
+        context.setCancelled();
+      }
+      return -1;
+    });
+    return returnValue != null ? returnValue : -1;
+  }
+
+  private static void closeQuietly(OutputStream stream) {
+    try {
+      stream.close();
+    } catch (IOException e) {
+      Throwables.propagate(e);
+    }
+  }
+
+  private int invokeCommand(BlazeContext context) {
+    String executingTasksText = "Command: "
+                                + Joiner.on(" ").join(command)
+                                + SystemProperties.getLineSeparator()
+                                + SystemProperties.getLineSeparator();
+
+    context.output(new PrintOutput(executingTasksText));
+
+    try {
+      if (context.isEnding()) {
+        return -1;
+      }
+      ProcessBuilder builder = new ProcessBuilder()
+        .command(command)
+        .redirectErrorStream(redirectErrorStream)
+        .directory(workingDirectory);
+      try {
+        final Process process = builder.start();
+        Thread shutdownHook = new Thread(process::destroy);
+        try {
+          Runtime.getRuntime().addShutdownHook(shutdownHook);
+          Thread stdoutThread = ProcessUtil.forwardAsync(process.getInputStream(), stdout);
+          Thread stderrThread = null;
+          if (!redirectErrorStream) {
+            stderrThread = ProcessUtil.forwardAsync(process.getErrorStream(), stderr);
+          }
+          process.waitFor();
+          stdoutThread.join();
+          if (!redirectErrorStream) {
+            stderrThread.join();
+          }
+          int exitValue = process.exitValue();
+          if (exitValue != 0) {
+            context.setHasError();
+          }
+          return exitValue;
+        }
+        catch (InterruptedException e) {
+          process.destroy();
+          throw new ProcessCanceledException();
+        }
+        finally {
+          try {
+            Runtime.getRuntime().removeShutdownHook(shutdownHook);
+          } catch (IllegalStateException e) {
+            // we can't remove a shutdown hook if we are shutting down, do nothing about it
+          }
+        }
+      }
+      catch (IOException e) {
+        LOG.warn(e);
+        IssueOutput.error(e.getMessage()).submit(context);
+      }
+    }
+    finally {
+      closeQuietly(stdout);
+      closeQuietly(stderr);
+    }
+    return -1;
+  }
+
+  public static Builder builder(
+    @NotNull File workingDirectory,
+    @NotNull List<String> command) {
+    return new Builder(workingDirectory, command);
+  }
+
+  public static Builder builder(
+    @NotNull WorkspaceRoot workspaceRoot,
+    @NotNull List<String> command) {
+    return new Builder(workspaceRoot, command);
+  }
+
+  public static Builder builder(
+    @NotNull WorkspaceRoot workspaceRoot,
+    @NotNull BlazeCommand command) {
+    return new Builder(workspaceRoot, command.toList());
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java b/blaze-base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java
new file mode 100644
index 0000000..a0bab3d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/process/LineProcessingOutputStream.java
@@ -0,0 +1,101 @@
+/*
+ * 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.async.process;
+
+import com.google.common.collect.Lists;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+/**
+ * An base output stream which marshals output into newline-delimited segments for processing.
+ */
+public final class LineProcessingOutputStream extends OutputStream {
+
+  public interface LineProcessor {
+    /**
+     * Process a single, complete line of output.
+     *
+     * @return Whether line processing should continue
+     */
+    boolean processLine(@NotNull String line);
+  }
+
+  @NotNull
+  private final StringBuffer stringBuffer = new StringBuffer();
+  private volatile boolean closed;
+  @NotNull
+  private final List<LineProcessor> lineProcessors;
+
+  LineProcessingOutputStream(@NotNull LineProcessor... lineProcessors) {
+    this.lineProcessors = Lists.newArrayList(lineProcessors);
+  }
+
+  public static LineProcessingOutputStream of(@NotNull LineProcessor... lineProcessors) {
+    return new LineProcessingOutputStream(lineProcessors);
+  }
+
+  @Override
+  public synchronized void write(byte[] b, int off, int len) {
+    if (!closed) {
+      String text = new String(b, off, len);
+      stringBuffer.append(text);
+
+      while (true) {
+        int lineBreakIndex = -1;
+        int lineBreakLength = 0;
+        for (int i = 0; i < stringBuffer.length(); ++i) {
+          char c = stringBuffer.charAt(i);
+          if (c == '\r' || c == '\n') {
+            lineBreakIndex = i;
+            lineBreakLength = 1;
+            if (c == '\r' && (i + 1) < stringBuffer.length() && stringBuffer.charAt(i + 1) == '\n') {
+              ++lineBreakLength;
+            }
+            break;
+          }
+        }
+
+        if (lineBreakIndex == -1) {
+          return;
+        }
+
+        String line = stringBuffer.substring(0, lineBreakIndex);
+
+        stringBuffer.delete(0, lineBreakIndex + lineBreakLength);
+
+        for (LineProcessor lineProcessor : lineProcessors) {
+          if (!lineProcessor.processLine(line)) {
+            break;
+          }
+        }
+      }
+    }
+  }
+
+  @Override
+  public void write(int b) throws IOException {
+    write(new byte[]{(byte)b}, 0, 1);
+  }
+
+  @Override
+  public void close() throws IOException {
+    closed = true;
+    super.close();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/process/PrintOutputLineProcessor.java b/blaze-base/src/com/google/idea/blaze/base/async/process/PrintOutputLineProcessor.java
new file mode 100644
index 0000000..562b648
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/process/PrintOutputLineProcessor.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.async.process;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Simple adapter between stdout and context print output.
+ */
+public class PrintOutputLineProcessor implements LineProcessingOutputStream.LineProcessor {
+  private final BlazeContext context;
+  public PrintOutputLineProcessor(BlazeContext context) {
+    this.context = context;
+  }
+  @Override
+  public boolean processLine(@NotNull String line) {
+    context.output(new PrintOutput(line));
+    return true;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/async/process/ProcessUtil.java b/blaze-base/src/com/google/idea/blaze/base/async/process/ProcessUtil.java
new file mode 100644
index 0000000..608f4d3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/async/process/ProcessUtil.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.async.process;
+
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+public class ProcessUtil {
+  private static final Logger LOG = Logger.getInstance(ProcessUtil.class);
+
+  public static Thread forwardAsync(final InputStream input, final OutputStream output) {
+    Thread thread = new Thread(new Runnable() {
+      @Override
+      public void run() {
+        int bufferSize = 4096;
+        byte[] buffer = new byte[bufferSize];
+
+        int read = 0;
+        try {
+          read = input.read(buffer);
+          while (read != -1) {
+            output.write(buffer, 0, read);
+            read = input.read(buffer);
+          }
+        }
+        catch (IOException e) {
+          LOG.warn("Error redirecting output", e);
+        }
+      }
+    });
+    thread.start();
+    return thread;
+  }
+
+  @NotNull
+  public static String runCommand(
+    @NotNull WorkspaceRoot workspaceRoot,
+    @NotNull List<String> command
+  ) {
+    ByteArrayOutputStream output = new ByteArrayOutputStream();
+    ExternalTask.Builder builder = ExternalTask.builder(workspaceRoot, command);
+    ExternalTask task = builder
+      .redirectStderr(true)
+      .stdout(output)
+      .build();
+    task.run();
+    return output.toString();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java b/blaze-base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
new file mode 100644
index 0000000..a0e22a2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/bazel/BazelBuildSystemProvider.java
@@ -0,0 +1,48 @@
+/*
+ * 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.bazel;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+
+/**
+ * Provides the bazel build system name string.
+ */
+public class BazelBuildSystemProvider implements BuildSystemProvider {
+  @Override
+  public BuildSystem buildSystem() {
+    return BuildSystem.Bazel;
+  }
+
+  @Override
+  public WorkspaceRootProvider getWorkspaceRootProvider() {
+    return BazelWorkspaceRootProvider.INSTANCE;
+  }
+
+  @Override
+  public ImmutableList<String> buildArtifactDirectories(WorkspaceRoot root) {
+    String rootDir = root.directory().getName();
+    return ImmutableList.of(
+      "bazel-bin",
+      "bazel-genfiles",
+      "bazel-out",
+      "bazel-testlogs",
+      "bazel-" + rootDir
+    );
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/bazel/BazelWorkspaceRootProvider.java b/blaze-base/src/com/google/idea/blaze/base/bazel/BazelWorkspaceRootProvider.java
new file mode 100644
index 0000000..95512d8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/bazel/BazelWorkspaceRootProvider.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.bazel;
+
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * Implementation of WorkspaceHelper.
+ */
+public class BazelWorkspaceRootProvider implements WorkspaceRootProvider {
+
+  public static final BazelWorkspaceRootProvider INSTANCE = new BazelWorkspaceRootProvider();
+
+  private BazelWorkspaceRootProvider() {}
+
+  /**
+   * Checks for the existence of a WORKSPACE file in the given directory.
+   */
+  @Override
+  public boolean isWorkspaceRoot(File file) {
+    return FileAttributeProvider.getInstance().isFile(new File(file, "WORKSPACE"));
+  }
+
+  @Nullable
+  @Override
+  public WorkspaceRoot findWorkspaceRoot(File file) {
+    while (file != null) {
+      if (isWorkspaceRoot(file)) {
+        return new WorkspaceRoot(file);
+      }
+      file = file.getParentFile();
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java b/blaze-base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
new file mode 100644
index 0000000..030e599
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/bazel/BuildSystemProvider.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.bazel;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.extensions.ExtensionPointName;
+
+import javax.annotation.Nullable;
+
+/**
+ * Extension points specify the build systems supported by this plugin.<br>
+ * The order of the extension points establishes a priority (highest priority first),
+ * for situations where we don't have an existing project to use for context
+ * (e.g. the 'import project' action).
+ */
+public interface BuildSystemProvider {
+
+  ExtensionPointName<BuildSystemProvider> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.BuildSystemProvider");
+
+  static BuildSystemProvider defaultBuildSystem() {
+    return EP_NAME.getExtensions()[0];
+  }
+
+  @Nullable
+  static BuildSystemProvider getBuildSystemProvider(BuildSystem buildSystem) {
+    for (BuildSystemProvider provider : EP_NAME.getExtensions()) {
+      if (provider.buildSystem() == buildSystem) {
+        return provider;
+      }
+    }
+    return null;
+  }
+
+  static boolean isBuildSystemAvailable(BuildSystem buildSystem) {
+    return getBuildSystemProvider(buildSystem) != null;
+  }
+
+  static WorkspaceRootProvider getWorkspaceRootProvider(BuildSystem buildSystem) {
+    BuildSystemProvider provider = getBuildSystemProvider(buildSystem);
+    if (provider == null) {
+      throw new RuntimeException(String.format("Build system '%s' not supported by this plugin", buildSystem));
+    }
+    return provider.getWorkspaceRootProvider();
+  }
+
+  static BuildSystemProvider getInstance() {
+    return ServiceManager.getService(BuildSystemProvider.class);
+  }
+
+  /**
+   * Returns the default build system for this application. This should only be
+   * called in situations where it doesn't make sense to use the current project.<br>
+   * Otherwise, use {@link com.google.idea.blaze.base.settings.Blaze#getBuildSystem}
+   */
+  BuildSystem buildSystem();
+
+  WorkspaceRootProvider getWorkspaceRootProvider();
+
+  /**
+   * Directories containing artifacts produced during the build process.
+   */
+  ImmutableList<String> buildArtifactDirectories(WorkspaceRoot root);
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/bazel/WorkspaceRootProvider.java b/blaze-base/src/com/google/idea/blaze/base/bazel/WorkspaceRootProvider.java
new file mode 100644
index 0000000..a9bf30d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/bazel/WorkspaceRootProvider.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.base.bazel;
+
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * Utility methods for working with workspace paths.
+ */
+public interface WorkspaceRootProvider {
+
+  /**
+   * Checks whether the given directory is a valid workspace root.
+   */
+  boolean isWorkspaceRoot(File file);
+
+  /**
+   * Checks whether the file or one of its ancestors is a valid workspace root.<br>
+   * This should only be called when no Project is available. Otherwise, use WorkspaceRoot.isInWorkspace.
+   */
+  default boolean isInWorkspace(File file) {
+    return findWorkspaceRoot(file) != null;
+  }
+
+  /**
+   * If the given file is inside a workspace, returns the workspace root. Otherwise returns null.
+   */
+  @Nullable
+  WorkspaceRoot findWorkspaceRoot(File file);
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmap/FileToBuildMap.java b/blaze-base/src/com/google/idea/blaze/base/buildmap/FileToBuildMap.java
new file mode 100644
index 0000000..ca19f19
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmap/FileToBuildMap.java
@@ -0,0 +1,59 @@
+/*
+ * 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.buildmap;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * Map of file -> BUILD files.
+ */
+public class FileToBuildMap {
+  private final Project project;
+
+  public static FileToBuildMap getInstance(Project project) {
+    return ServiceManager.getService(project, FileToBuildMap.class);
+  }
+
+  public FileToBuildMap(Project project) {
+    this.project = project;
+  }
+
+  public Collection<File> getBuildFilesForFile(File file) {
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return ImmutableList.of();
+    }
+    return SourceToRuleMap.getInstance(project).getTargetsForSourceFile(file)
+      .stream()
+      .map(blazeProjectData.ruleMap::get)
+      .filter(Objects::nonNull)
+      .map((ruleIdeInfo) -> ruleIdeInfo.buildFile)
+      .filter(Objects::nonNull)
+      .map(ArtifactLocation::getFile)
+      .collect(Collectors.toList());
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmap/OpenCorrespondingBuildFile.java b/blaze-base/src/com/google/idea/blaze/base/buildmap/OpenCorrespondingBuildFile.java
new file mode 100644
index 0000000..afb3c62
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmap/OpenCorrespondingBuildFile.java
@@ -0,0 +1,72 @@
+/*
+ * 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.buildmap;
+
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.metrics.LoggingService;
+import com.intellij.ide.actions.OpenFileAction;
+import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collection;
+
+public class OpenCorrespondingBuildFile extends BlazeAction {
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project == null) {
+      return;
+    }
+    VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE);
+    File file = getBuildFile(project, virtualFile);
+    if (file == null) {
+      return;
+    }
+    OpenFileAction.openFile(file.getPath(), project);
+    LoggingService.reportEvent(project, Action.OPEN_CORRESPONDING_BUILD_FILE);
+  }
+
+  @Nullable
+  private File getBuildFile(@Nullable Project project, @Nullable VirtualFile virtualFile) {
+    if (project == null) {
+      return null;
+    }
+    if (virtualFile == null) {
+      return null;
+    }
+    File file = new File(virtualFile.getPath());
+    Collection<File> fileInfoList = FileToBuildMap.getInstance(project).getBuildFilesForFile(file);
+    return Iterables.getFirst(fileInfoList, null);
+  }
+
+  @Override
+  protected void doUpdate(@NotNull AnActionEvent e) {
+    Presentation presentation = e.getPresentation();
+    DataContext dataContext = e.getDataContext();
+    VirtualFile virtualFile = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
+    Project project = CommonDataKeys.PROJECT.getData(dataContext);
+    boolean visible = (project != null && virtualFile != null);
+    boolean enabled = getBuildFile(project, virtualFile) != null;
+    presentation.setVisible(visible || ActionPlaces.isMainMenuOrActionSearch(e.getPlace()));
+    presentation.setEnabled(enabled);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BazelBuildifierBinaryProvider.java b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BazelBuildifierBinaryProvider.java
new file mode 100644
index 0000000..caf6389
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BazelBuildifierBinaryProvider.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.buildmodifier;
+
+import com.google.idea.blaze.base.util.BlazeHelperBinaryUtil;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * Provides the bazel buildifier binary.
+ */
+public class BazelBuildifierBinaryProvider implements BuildifierBinaryProvider {
+
+  private static final String BUILDIFIER_BINARY_PATH = "/blaze-base/resources/binaries/bazel-buildifier";
+
+  @Nullable
+  @Override
+  public File getBuildifierBinary() {
+    return BlazeHelperBinaryUtil.getBlazeHelperBinary(BUILDIFIER_BINARY_PATH);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildFileFormatter.java b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildFileFormatter.java
new file mode 100644
index 0000000..bb7f66d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildFileFormatter.java
@@ -0,0 +1,83 @@
+/*
+ * 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 javax.annotation.Nullable;
+import java.io.*;
+
+/**
+ * Formats BUILD files using 'buildifier'
+ */
+public class BuildFileFormatter {
+
+  @Nullable
+  private static File getBuildifierBinary() {
+    for (BuildifierBinaryProvider provider : BuildifierBinaryProvider.EP_NAME.getExtensions()) {
+      File file = provider.getBuildifierBinary();
+      if (file != null) {
+        return file;
+      }
+    }
+    return null;
+  }
+
+  static String formatText(String text) {
+    try {
+      File buildifierBinary = getBuildifierBinary();
+      if (buildifierBinary == null) {
+        return null;
+      }
+      File file = createTempFile(text);
+      formatFile(file, buildifierBinary.getPath());
+      String formattedFile = readFile(file);
+      file.delete();
+      return formattedFile;
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+    return text;
+  }
+
+  private static void formatFile(File file, String buildifierBinaryPath) throws IOException {
+    ProcessBuilder builder = new ProcessBuilder(buildifierBinaryPath, file.getAbsolutePath());
+    try {
+      builder.start().waitFor();
+    } catch (InterruptedException e) {
+      throw new IOException("buildifier execution failed", e);
+    }
+  }
+
+
+  private static String readFile(File file) throws IOException {
+    try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
+      StringBuilder formattedFile = new StringBuilder();
+      char[] buf = new char[1024];
+      int numRead;
+      while ((numRead = reader.read(buf)) >= 0) {
+        formattedFile.append(buf, 0, numRead);
+      }
+      return formattedFile.toString();
+    }
+  }
+
+  private static File createTempFile(String text) throws IOException {
+    File file = File.createTempFile("ijwb", ".tmp");
+    try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) {
+      writer.write(text);
+    }
+    return file;
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildFileModifier.java b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildFileModifier.java
new file mode 100644
index 0000000..c8c570c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildFileModifier.java
@@ -0,0 +1,46 @@
+/*
+ * 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.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+public interface BuildFileModifier {
+
+  static BuildFileModifier getInstance() {
+    return ServiceManager.getService(BuildFileModifier.class);
+  }
+
+  /**
+   * Add a new rule to a build file. The rule name and rule kind must be validated before this
+   * method, but no guarantees are made about actually being able to add this rule to the build
+   * file. An example of why it might fail is the build file might already have a rule with the
+   * requested name.
+   *
+   * @param context  Blaze context to operate in
+   * @param newRule  new rule to create
+   * @param ruleKind valid kind of rule (android_library, java_library, etc.)
+   * @return true if rule is added to file, false otherwise
+   */
+  boolean addRule(
+    Project project,
+    final BlazeContext context,
+    final Label newRule,
+    final Kind ruleKind);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildifierBinaryProvider.java b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildifierBinaryProvider.java
new file mode 100644
index 0000000..3390002
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/BuildifierBinaryProvider.java
@@ -0,0 +1,32 @@
+/*
+ * 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.intellij.openapi.extensions.ExtensionPointName;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * Provides a buildifier binary.
+ */
+public interface BuildifierBinaryProvider {
+
+  ExtensionPointName<BuildifierBinaryProvider> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.BuildifierBinaryProvider");
+
+  @Nullable
+  File getBuildifierBinary();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
new file mode 100644
index 0000000..c77d34b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSaveHandler.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.buildmodifier;
+
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.command.CommandProcessor;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.DocumentRunnable;
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.openapi.fileEditor.FileDocumentManagerAdapter;
+import com.intellij.openapi.vfs.VirtualFile;
+
+/**
+ * Runs the buildifier command on file save.
+ */
+public class FileSaveHandler extends FileDocumentManagerAdapter {
+
+  @Override
+  public void beforeDocumentSaving(final Document document) {
+    if (!document.isWritable()) {
+      return;
+    }
+    FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
+    VirtualFile file = fileDocumentManager.getFile(document);
+    if (file == null || !file.isValid()) {
+      return;
+    }
+
+    if (!isBuildFile(file)) {
+      return;
+    }
+    int lines = document.getLineCount();
+    if (lines > 0) {
+      String text = document.getText();
+      String formattedText = BuildFileFormatter.formatText(text);
+      updateDocument(document, formattedText);
+    }
+  }
+
+  private void updateDocument(final Document document, final String formattedContent) {
+    ApplicationManager.getApplication().runWriteAction(new DocumentRunnable(document, null) {
+      @Override
+      public void run() {
+        CommandProcessor.getInstance().runUndoTransparentAction(() -> document.setText(formattedContent));
+      }
+    });
+  }
+
+  private static boolean isBuildFile(VirtualFile file) {
+    return file.getName().equals("BUILD");
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
new file mode 100644
index 0000000..6606b23
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifier.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.buildmodifier;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+public abstract class FileSystemModifier {
+
+  public static FileSystemModifier getInstance(@NotNull Project project) {
+    return ServiceManager.getService(project, FileSystemModifier.class);
+  }
+
+  @Nullable
+  public abstract File makeWorkspacePathDirs(@NotNull WorkspacePath workspacePath);
+
+  @Nullable
+  public abstract File createFile(@NotNull WorkspacePath parentDir, @NotNull String name);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
new file mode 100644
index 0000000..40470d7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/buildmodifier/FileSystemModifierImpl.java
@@ -0,0 +1,61 @@
+/*
+ * 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.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+
+public class FileSystemModifierImpl extends FileSystemModifier {
+
+  private final Project project;
+
+  public FileSystemModifierImpl(@NotNull Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public File makeWorkspacePathDirs(@NotNull WorkspacePath workspacePath) {
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    File dir = workspaceRoot.fileForPath(workspacePath);
+    boolean success = dir.mkdirs();
+    return success ? dir : null;
+  }
+
+  @Nullable
+  @Override
+  public File createFile(
+    @NotNull WorkspacePath parentDirectory,
+    @NotNull String name) {
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    File dir = workspaceRoot.fileForPath(parentDirectory);
+    File f = new File(dir, name);
+    boolean success;
+    try {
+      success = f.createNewFile();
+    }
+    catch (IOException e) {
+      success = false;
+    }
+    return success ? f : null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/BlazeCommand.java b/blaze-base/src/com/google/idea/blaze/base/command/BlazeCommand.java
new file mode 100644
index 0000000..21a8a4d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/BlazeCommand.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.Immutable;
+
+/**
+ * A command to issue to Blaze/Bazel on the command line.
+ */
+@Immutable
+public final class BlazeCommand {
+
+  private final BuildSystem buildSystem;
+  private final BlazeCommandName name;
+  private final ImmutableList<String> arguments;
+
+  private BlazeCommand(BuildSystem buildSystem, BlazeCommandName name, ImmutableList<String> arguments) {
+    this.buildSystem = buildSystem;
+    this.name = name;
+    this.arguments = arguments;
+  }
+
+  public BlazeCommandName getName() {
+    return name;
+  }
+
+  public ImmutableList<String> toList() {
+    ImmutableList.Builder<String> commandLine = ImmutableList.builder();
+    commandLine.add(getBinaryPath(buildSystem), name.toString());
+    commandLine.addAll(arguments);
+    return commandLine.build();
+  }
+
+  private static String getBinaryPath(BuildSystem buildSystem) {
+    BlazeUserSettings settings = BlazeUserSettings.getInstance();
+    switch (buildSystem) {
+      case Blaze:
+        return settings.getBlazeBinaryPath();
+      case Bazel:
+        return settings.getBazelBinaryPath();
+      default:
+        throw new RuntimeException("Unrecognized build system type: " + buildSystem);
+    }
+  }
+
+  @Override
+  public String toString() {
+    return Joiner.on(' ').join(toList());
+  }
+
+  public static Builder builder(BuildSystem buildSystem, BlazeCommandName name) {
+    return new Builder(buildSystem, name);
+  }
+
+  public static class Builder {
+    private final BuildSystem buildSystem;
+    private final BlazeCommandName name;
+    private final ImmutableList.Builder<TargetExpression> targets = ImmutableList.builder();
+    private final ImmutableList.Builder<String> blazeFlags = ImmutableList.builder();
+    private final ImmutableList.Builder<String> exeFlags = ImmutableList.builder();
+
+    public Builder(BuildSystem buildSystem, BlazeCommandName name) {
+      this.buildSystem = buildSystem;
+      this.name = name;
+      // Tell forge what tool we used to call blaze so we can track usage.
+      addBlazeFlags(BlazeFlags.getToolTagFlag());
+    }
+
+      public BlazeCommand build() {
+      ImmutableList.Builder<String> arguments = ImmutableList.builder();
+      arguments.addAll(blazeFlags.build());
+      arguments.add("--");
+
+      // Trust the user's ordering of the targets since order matters to blaze
+      for (TargetExpression targetExpression : targets.build()) {
+        arguments.add(targetExpression.toString());
+      }
+
+      arguments.addAll(exeFlags.build());
+      return new BlazeCommand(buildSystem, name, arguments.build());
+    }
+
+      public Builder addTargets(TargetExpression... targets) {
+      return this.addTargets(Arrays.asList(targets));
+    }
+
+      public Builder addTargets(List<? extends TargetExpression> targets) {
+      this.targets.addAll(targets);
+      return this;
+    }
+
+      public Builder addExeFlags(String... flags) {
+      return addExeFlags(Arrays.asList(flags));
+    }
+
+      public Builder addExeFlags(List<String> flags) {
+      this.exeFlags.addAll(flags);
+      return this;
+    }
+
+      public Builder addBlazeFlags(String... flags) {
+      return addBlazeFlags(Arrays.asList(flags));
+    }
+
+      public Builder addBlazeFlags(List<String> flags) {
+      this.blazeFlags.addAll(flags);
+      return this;
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/BlazeCommandName.java b/blaze-base/src/com/google/idea/blaze/base/command/BlazeCommandName.java
new file mode 100644
index 0000000..dc27d08
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/BlazeCommandName.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.concurrent.Immutable;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A class for Blaze/Bazel command names. We enumerate the commands we use (and that we expect users to be
+ * interested in), but do NOT use an enum because we want to allow users to specify arbitrary
+ * commands.
+ */
+@Immutable
+public final class BlazeCommandName {
+  @NotNull
+  private static final ConcurrentMap<String, BlazeCommandName> knownCommands = Maps.newConcurrentMap();
+
+  public static final BlazeCommandName BUILD = fromString("build");
+  public static final BlazeCommandName TEST = fromString("test");
+  public static final BlazeCommandName MOBILE_INSTALL = fromString("mobile-install");
+  public static final BlazeCommandName RUN = fromString("run");
+  public static final BlazeCommandName QUERY = fromString("query");
+  public static final BlazeCommandName INFO = fromString("info");
+
+
+  public static BlazeCommandName fromString(@NotNull String name) {
+    knownCommands.putIfAbsent(name, new BlazeCommandName(name));
+    return knownCommands.get(name);
+  }
+
+  private final String name;
+
+  private BlazeCommandName(@NotNull String name) {
+    Preconditions.checkArgument(!name.isEmpty(), "Command should be non-empty.");
+    this.name = name;
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof BlazeCommandName)) {
+      return false;
+    }
+    BlazeCommandName that = (BlazeCommandName)o;
+    return name.equals(that.name);
+  }
+
+  @Override
+  public int hashCode() {
+    return name.hashCode();
+  }
+
+  /**
+   * @return An unmodifiable view of the Blaze commands we know about (including those that the user
+   * has specified, in addition to those we have hard-coded).
+   */
+  @NotNull
+  public static Collection<BlazeCommandName> knownCommands() {
+    return ImmutableList.copyOf(knownCommands.values());
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/BlazeFlags.java b/blaze-base/src/com/google/idea/blaze/base/command/BlazeFlags.java
new file mode 100644
index 0000000..7e83d40
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/BlazeFlags.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.command;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.sections.BuildFlagsSection;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.util.PlatformUtils;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * The collection of all the Bazel flag strings we use.
+ */
+public final class BlazeFlags {
+
+  // Build the maximum number of possible dependencies of the project and to show all the build
+  // errors in single go.
+  public static final String KEEP_GOING = "--keep_going";
+  // Tells Blaze to open a debug port and wait for a connection while running tests
+  // It expands to: --test_arg=--wrapper_script_flag=--debug --test_output=streamed
+  //   --test_strategy=exclusive --test_timeout=9999 --nocache_test_results
+  public static final String JAVA_TEST_DEBUG = "--java_debug";
+  // Tells the Java wrapper stub to launch JVM in remote debugging mode, waiting for a connection
+  public static final String JAVA_BINARY_DEBUG = "--debug";
+  // Runs tests locally, in sequence (rather than parallel), and streams their results to stdout.
+  public static final String TEST_OUTPUT_STREAMED = "--test_output=streamed";
+  // Filters the unit tests that are run (used with regexp for Java/Robolectric tests).
+  public static final String TEST_FILTER = "--test_filter";
+  // Skips checking for output file modifications (reduced statting -> faster).
+  public static final String NO_CHECK_OUTPUTS = "--noexperimental_check_output_files";
+  // Ignores implicit dependencies (e.g. java_library rules depending implicitly on
+  // "//transconsole/tools:aggregate_messages" in order to support translations).
+  public static final String NO_IMPLICIT_DEPS = "--noimplicit_deps";
+  // Ignores host dependencies.
+  public static final String NO_HOST_DEPS = "--nohost_deps";
+  // When used with mobile-install, deploys the an app incrementally.
+  public static final String INCREMENTAL = "--incremental";
+  // When used with mobile-install, deploys the an app incrementally
+  // can be used for API 23 or higher, for which it is preferred to --incremental
+  public static final String SPLIT_APKS = "--split_apks";
+  // Re-run the test even if the results are cached.
+  public static final String NO_CACHE_TEST_RESULTS = "--nocache_test_results";
+  // Avoids node GC between ide_build_info and blaze build
+  public static final String VERSION_WINDOW_FOR_DIRTY_NODE_GC =
+    "--version_window_for_dirty_node_gc=-1";
+
+  public static final String EXPERIMENTAL_SHOW_ARTIFACTS =
+    "--experimental_show_artifacts";
+
+  public static List<String> buildFlags(Project project, ProjectViewSet projectViewSet) {
+    BuildSystem buildSystem = Blaze.getBuildSystem(project);
+    List<String> flags = Lists.newArrayList();
+    for (BuildFlagsProvider buildFlagsProvider : BuildFlagsProvider.EP_NAME.getExtensions()) {
+      buildFlagsProvider.addBuildFlags(buildSystem, projectViewSet, flags);
+    }
+    flags.addAll(projectViewSet.listItems(BuildFlagsSection.KEY));
+    return flags;
+  }
+
+  // Pass-through arg for sending adb options during mobile-install.
+  public static final String ADB_ARG = "--adb_arg=";
+
+  public static ImmutableList<String> adbSerialFlags(String serial) {
+    return ImmutableList.of(ADB_ARG + "-s ", ADB_ARG + serial);
+  }
+
+  // Pass-through arg for sending test arguments.
+  public static final String TEST_ARG = "--test_arg=";
+
+  private static final String TOOL_TAG = "--tool_tag=ijwb:";
+
+  // We add this to every single BlazeCommand instance. It's for tracking usage.
+  public static String getToolTagFlag() {
+    String platformPrefix = PlatformUtils.getPlatformPrefix();
+
+    // IDEA Community Edition is "Idea", whereas IDEA Ultimate Edition is "idea". That's dumb. Let's make them more useful.
+    if (PlatformUtils.isIdeaCommunity()) {
+      platformPrefix = "IDEA:community";
+    } else if (PlatformUtils.isIdeaUltimate()) {
+      platformPrefix = "IDEA:ultimate";
+    }
+    return TOOL_TAG + platformPrefix;
+  }
+
+  public static String testFilterFlagForClass(String className) {
+    return testFilterFlagForClassAndMethod(className, null);
+  }
+
+  public static String testFilterFlagForClassAndMethod(String className, @Nullable String methodName) {
+    StringBuilder output = new StringBuilder(TEST_FILTER);
+    output.append('=');
+    output.append(className);
+
+    if (!Strings.isNullOrEmpty(methodName)) {
+      output.append('#');
+      output.append(methodName);
+    }
+
+    return output.toString();
+  }
+
+  private BlazeFlags() {
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/BlazeQuery.java b/blaze-base/src/com/google/idea/blaze/base/command/BlazeQuery.java
new file mode 100644
index 0000000..cb5c583
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/BlazeQuery.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command;
+
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.OutputStream;
+
+/**
+ * Code for issuing a blaze query.
+ */
+public class BlazeQuery {
+
+  public static void query(
+    @NotNull final Project project,
+    @NotNull BlazeContext context,
+    @NotNull final String query,
+    @NotNull OutputStream stdout) {
+    BlazeImportSettings importSettings = BlazeImportSettingsManager.getInstance(project)
+      .getImportSettings();
+    assert importSettings != null;
+    final WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+
+    final BlazeCommand command = BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.QUERY)
+      .addBlazeFlags(BlazeFlags.KEEP_GOING, BlazeFlags.NO_IMPLICIT_DEPS, BlazeFlags.NO_HOST_DEPS)
+      .addBlazeFlags(query)
+      .build();
+
+    ExternalTask.builder(workspaceRoot, command)
+      .context(context)
+      .stdout(stdout)
+      .stderr(LineProcessingOutputStream.of(new IssueOutputLineProcessor(project, context, workspaceRoot)))
+      .build()
+      .run();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/BuildFlagsProvider.java b/blaze-base/src/com/google/idea/blaze/base/command/BuildFlagsProvider.java
new file mode 100644
index 0000000..2533bdc
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/BuildFlagsProvider.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.command;
+
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.extensions.ExtensionPointName;
+
+import java.util.List;
+
+/**
+ * Provides additional build flags for bazel/blaze commands.
+ */
+public interface BuildFlagsProvider {
+
+  ExtensionPointName<BuildFlagsProvider> EP_NAME = ExtensionPointName
+    .create("com.google.idea.blaze.BuildFlagsProvider");
+
+  void addBuildFlags(BuildSystem buildSystem, ProjectViewSet projectViewSet, List<String> flags);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java b/blaze-base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
new file mode 100644
index 0000000..f6c3a7f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/BuildFlagsProviderImpl.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command;
+
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.experiments.IntExperiment;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+
+import java.util.List;
+
+import static com.google.idea.blaze.base.command.BlazeFlags.NO_CHECK_OUTPUTS;
+import static com.google.idea.blaze.base.command.BlazeFlags.VERSION_WINDOW_FOR_DIRTY_NODE_GC;
+
+/**
+ * Flags added to blaze/bazel build commands.
+ */
+public class BuildFlagsProviderImpl implements BuildFlagsProvider {
+
+  private static final BoolExperiment EXPERIMENT_USE_VERSION_WINDOW_FOR_DIRTY_NODE_GC =
+    new BoolExperiment("ide_build_info.use_version_window_for_dirty_node_gc", false);
+  private static final BoolExperiment EXPERIMENT_NO_EXPERIMENTAL_CHECK_OUTPUT_FILES =
+    new BoolExperiment("build.noexperimental_check_output_files", false);
+  private static final IntExperiment EXPERIMENT_MIN_PKG_COUNT_FOR_CT_NODE_EVICTION =
+    new IntExperiment("min_pkg_count_for_ct_node_eviction", 0);
+
+  // Avoids blaze state invalidation from non-overlapping transitive closures
+  // This is caused by our search for the android_sdk
+  private static final String MIN_PKG_COUNT_FOR_CT_NODE_EVICTION =
+    "--min_pkg_count_for_ct_node_eviction=";
+
+  private static String minPkgCountForCtNodeEviction(int value) {
+    return MIN_PKG_COUNT_FOR_CT_NODE_EVICTION + value;
+  }
+
+  @Override
+  public void addBuildFlags(BuildSystem buildSystem, ProjectViewSet projectViewSet, List<String> flags) {
+    if (EXPERIMENT_USE_VERSION_WINDOW_FOR_DIRTY_NODE_GC.getValue()) {
+      flags.add(VERSION_WINDOW_FOR_DIRTY_NODE_GC);
+    }
+    if (EXPERIMENT_NO_EXPERIMENTAL_CHECK_OUTPUT_FILES.getValue()) {
+      flags.add(NO_CHECK_OUTPUTS);
+    }
+    int minPkgCountForCtNodeEviction = EXPERIMENT_MIN_PKG_COUNT_FOR_CT_NODE_EVICTION.getValue();
+    if (minPkgCountForCtNodeEviction > 0) {
+      flags.add(minPkgCountForCtNodeEviction(minPkgCountForCtNodeEviction));
+    }
+  }
+
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java b/blaze-base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
new file mode 100644
index 0000000..b8d82d2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/ExperimentalShowArtifactsLineProcessor.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command;
+
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Collects the output of --experimental_show_artifacts
+ */
+public class ExperimentalShowArtifactsLineProcessor implements LineProcessingOutputStream.LineProcessor {
+  private static final String OUTPUT_START = "Build artifacts:";
+  private static final String OUTPUT_MARKER = ">>>";
+
+  final List<File> fileList;
+  private final String fileType;
+  boolean insideBuildResult = false;
+
+  public ExperimentalShowArtifactsLineProcessor(List<File> fileList,
+                                                String fileType) {
+    this.fileList = fileList;
+    this.fileType = fileType;
+  }
+
+  @Override
+  public boolean processLine(@NotNull String line) {
+    if (insideBuildResult) {
+      // Workaround for --experimental_ui: Extra newlines are inserted
+      if (line.isEmpty()) {
+        return false;
+      }
+
+      insideBuildResult = line.startsWith(OUTPUT_MARKER);
+      if (insideBuildResult) {
+        String fileName = line.substring(OUTPUT_MARKER.length());
+        if (fileName.endsWith(fileType)) {
+          fileList.add(new File(fileName));
+        }
+      }
+    }
+    if (!insideBuildResult) {
+      insideBuildResult = line.equals(OUTPUT_START);
+    }
+    return !insideBuildResult;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java b/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
new file mode 100644
index 0000000..510d042
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfo.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.info;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.components.ServiceManager;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Runs the blaze info command. The results may be cached in the workspace.
+ */
+public abstract class BlazeInfo {
+  public static final String EXECUTION_ROOT_KEY = "execution_root";
+  public static final String PACKAGE_PATH_KEY = "package_path";
+  public static final String BUILD_LANGUAGE = "build-language";
+
+  public static String blazeBinKey(BuildSystem buildSystem) {
+    switch (buildSystem) {
+      case Blaze:
+        return "blaze-bin";
+      case Bazel:
+        return "bazel-bin";
+      default:
+        throw new IllegalArgumentException("Unrecognized build system: " + buildSystem);
+    }
+  }
+
+  public static String blazeGenfilesKey(BuildSystem buildSystem) {
+    switch (buildSystem) {
+      case Blaze:
+        return "blaze-genfiles";
+      case Bazel:
+        return "bazel-genfiles";
+      default:
+        throw new IllegalArgumentException("Unrecognized build system: " + buildSystem);
+    }
+  }
+
+  public static BlazeInfo getInstance() {
+    return ServiceManager.getService(BlazeInfo.class);
+  }
+
+  /**
+   * @param blazeFlags The blaze flags that will be passed to Blaze.
+   * @param key The key passed to blaze info
+   * @return The blaze info value associated with the specified key
+   */
+  public abstract ListenableFuture<String> runBlazeInfo(
+    @Nullable BlazeContext context,
+    BuildSystem buildSystem,
+    WorkspaceRoot workspaceRoot,
+    List<String> blazeFlags,
+    String key);
+
+  /**
+   * @param blazeFlags The blaze flags that will be passed to Blaze.
+   * @param key The key passed to blaze info
+   * @return The blaze info value associated with the specified key
+   */
+  public abstract ListenableFuture<byte[]> runBlazeInfoGetBytes(
+    @Nullable BlazeContext context,
+    BuildSystem buildSystem,
+    WorkspaceRoot workspaceRoot,
+    List<String> blazeFlags,
+    String key);
+
+  /**
+   * This calls blaze info without any specific key so blaze info will return all keys and values that it has. There could be a performance
+   * cost for doing this, so the user should verify that calling this method is actually faster than calling
+   * {@link #runBlazeInfo(WorkspaceRoot, List, String)}.
+   *
+   * @param blazeFlags The blaze flags that will be passed to Blaze.
+   * @return The blaze info data fields.
+   */
+  public abstract ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(
+    @Nullable BlazeContext context,
+    BuildSystem buildSystem,
+    WorkspaceRoot workspaceRoot,
+    List<String> blazeFlags);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java b/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java
new file mode 100644
index 0000000..aeef5a1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfoException.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.info;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public final class BlazeInfoException extends Exception {
+  private final int exitCode;
+  private final String stdout;
+  private final String stderr;
+
+  public BlazeInfoException(int exitCode, String stdout, String stderr) {
+    this.exitCode = exitCode;
+    this.stdout = stdout;
+    this.stderr = stderr;
+  }
+
+  @Override
+  public String getMessage() {
+    return "blaze info failed with exit code: " + exitCode + " and error stream: " + stderr;
+  }
+
+  public int getExitCode() {
+    return exitCode;
+  }
+
+  public String getStdout() {
+    return stdout;
+  }
+
+  public String getStderr() {
+    return stderr;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java b/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java
new file mode 100644
index 0000000..285f276
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/command/info/BlazeInfoImpl.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command.info;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.diagnostic.Logger;
+
+import javax.annotation.Nullable;
+import java.io.ByteArrayOutputStream;
+import java.util.List;
+
+public class BlazeInfoImpl extends BlazeInfo {
+  private static final Logger LOG = Logger.getInstance(BlazeInfoImpl.class);
+
+  @Override
+  public ListenableFuture<String> runBlazeInfo(@Nullable BlazeContext context,
+                                               BuildSystem buildSystem,
+                                               WorkspaceRoot workspaceRoot,
+                                               List<String> blazeFlags,
+                                               String key) {
+    return BlazeExecutor.getInstance().submit(() -> runBlazeInfo(buildSystem, workspaceRoot, key, blazeFlags, context).toString().trim());
+  }
+
+  @Override
+  public ListenableFuture<byte[]> runBlazeInfoGetBytes(@Nullable BlazeContext context,
+                                                       BuildSystem buildSystem,
+                                                       WorkspaceRoot workspaceRoot,
+                                                       List<String> blazeFlags,
+                                                       String key) {
+    return BlazeExecutor.getInstance().submit(() -> runBlazeInfo(buildSystem, workspaceRoot, key, blazeFlags, context).toByteArray());
+  }
+
+  @Override
+  public ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(@Nullable BlazeContext context,
+                                                                     BuildSystem buildSystem,
+                                                                     WorkspaceRoot workspaceRoot,
+                                                                     List<String> blazeFlags) {
+    return BlazeExecutor.getInstance().submit(() -> {
+      String blazeInfoString = runBlazeInfo(buildSystem, workspaceRoot, null /* key */, blazeFlags, context).toString().trim();
+      return parseBlazeInfoResult(blazeInfoString);
+    });
+  }
+
+  private static ByteArrayOutputStream runBlazeInfo(
+    BuildSystem buildSystem,
+    WorkspaceRoot workspaceRoot,
+    @Nullable String key,
+    List<String> blazeFlags,
+    @Nullable BlazeContext context) throws BlazeInfoException {
+    BlazeCommand.Builder builder = BlazeCommand.builder(buildSystem, BlazeCommandName.INFO);
+    if (key != null) {
+      builder.addBlazeFlags(key);
+    }
+    BlazeCommand command = builder.addBlazeFlags(blazeFlags).build();
+    ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+    ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+    int exitCode = ExternalTask.builder(workspaceRoot, command)
+      .context(context)
+      .stdout(stdout)
+      .stderr(stderr)
+      .build()
+      .run();
+    if (exitCode != 0) {
+      throw new BlazeInfoException(exitCode, stdout.toString(), stderr.toString());
+    }
+    return stdout;
+  }
+
+  private static ImmutableMap<String, String> parseBlazeInfoResult(String blazeInfoString) {
+    ImmutableMap.Builder<String, String> blazeInfoMapBuilder = ImmutableMap.builder();
+    String[] blazeInfoLines = blazeInfoString.split("\n");
+    for (String blazeInfoLine : blazeInfoLines) {
+      // Just split on the first ":".
+      String[] keyValue = blazeInfoLine.split(":", 2);
+      LOG.assertTrue(keyValue.length == 2, blazeInfoLine);
+      String key = keyValue[0].trim();
+      String value = keyValue[1].trim();
+      blazeInfoMapBuilder.put(key, value);
+    }
+    return blazeInfoMapBuilder.build();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java b/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java
new file mode 100644
index 0000000..3a92c75
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleService.java
@@ -0,0 +1,39 @@
+/*
+ * 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.console;
+
+import com.intellij.execution.ui.ConsoleViewContentType;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Prints text to the blaze console.
+ */
+public interface BlazeConsoleService {
+  static BlazeConsoleService getInstance(@NotNull Project project) {
+    return ServiceManager.getService(project, BlazeConsoleService.class);
+  }
+
+  void print(@NotNull String text, @NotNull ConsoleViewContentType contentType);
+
+  void clear();
+
+  void setStopHandler(@Nullable Runnable runnable);
+
+  void activateConsoleWindow();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java b/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java
new file mode 100644
index 0000000..61e8826
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleServiceImpl.java
@@ -0,0 +1,60 @@
+/*
+ * 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.console;
+
+import com.intellij.execution.ui.ConsoleViewContentType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Implementation for BlazeConsoleService
+ */
+public class BlazeConsoleServiceImpl implements BlazeConsoleService {
+  @NotNull private final Project project;
+  @NotNull private final BlazeConsoleView blazeConsoleView;
+
+  BlazeConsoleServiceImpl(@NotNull Project project) {
+    this.project = project;
+    blazeConsoleView = BlazeConsoleView.getInstance(project);
+  }
+
+  @Override
+  public void print(@NotNull String text, @NotNull ConsoleViewContentType contentType) {
+    blazeConsoleView.print(text, contentType);
+  }
+
+  @Override
+  public void clear() {
+    blazeConsoleView.clear();
+  }
+
+  @Override
+  public void setStopHandler(@Nullable Runnable runnable) {
+    blazeConsoleView.setStopHandler(runnable);
+  }
+
+  @Override
+  public void activateConsoleWindow() {
+    ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow(
+      BlazeConsoleToolWindowFactory.ID);
+    if (toolWindow != null) {
+      toolWindow.activate(null);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleToolWindowFactory.java b/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleToolWindowFactory.java
new file mode 100644
index 0000000..2ff82fa
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleToolWindowFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.console;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowFactory;
+import org.jetbrains.annotations.NotNull;
+
+public class BlazeConsoleToolWindowFactory implements DumbAware, ToolWindowFactory {
+
+  public static final String ID = "Blaze Console";
+
+  @Override
+  public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
+    String title = Blaze.buildSystemName(project) + " Console";
+    toolWindow.setTitle(title);
+    toolWindow.setStripeTitle(title);
+    BlazeConsoleView.getInstance(project).createToolWindowContent(toolWindow);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java b/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleView.java
new file mode 100644
index 0000000..ebbb630
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/console/BlazeConsoleView.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.console;
+
+import com.intellij.codeEditor.printing.PrintAction;
+import com.intellij.execution.impl.ConsoleViewImpl;
+import com.intellij.execution.ui.ConsoleViewContentType;
+import com.intellij.execution.ui.RunnerLayoutUi;
+import com.intellij.execution.ui.layout.PlaceInGrid;
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.IdeBundle;
+import com.intellij.ide.actions.NextOccurenceToolbarAction;
+import com.intellij.ide.actions.PreviousOccurenceToolbarAction;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.DumbAwareAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.ui.content.Content;
+import com.intellij.ui.content.ContentFactory;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+
+public class BlazeConsoleView implements Disposable {
+
+  private static final Class<?>[] IGNORED_CONSOLE_ACTION_TYPES =
+    {PreviousOccurenceToolbarAction.class, NextOccurenceToolbarAction.class,
+      ConsoleViewImpl.ClearAllAction.class, PrintAction.class};
+
+  @NotNull
+  private final Project myProject;
+  @NotNull
+  private final ConsoleViewImpl myConsoleView;
+
+  private JPanel myConsolePanel;
+  private volatile Runnable myStopHandler;
+
+  public BlazeConsoleView(@NotNull Project project) {
+    myProject = project;
+    myConsoleView = new ConsoleViewImpl(myProject, false);
+    Disposer.register(this, myConsoleView);
+    setupUI();
+  }
+
+  public static BlazeConsoleView getInstance(@NotNull Project project) {
+    return ServiceManager.getService(project, BlazeConsoleView.class);
+  }
+
+  public void setStopHandler(@Nullable Runnable stopHandler) {
+    myStopHandler = stopHandler;
+  }
+
+  private static boolean shouldIgnoreAction(@NotNull AnAction action) {
+    for (Class<?> actionType : IGNORED_CONSOLE_ACTION_TYPES) {
+      if (actionType.isInstance(action)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public void createToolWindowContent(@NotNull ToolWindow toolWindow) {
+    //Create runner UI layout
+    RunnerLayoutUi.Factory factory = RunnerLayoutUi.Factory.getInstance(myProject);
+    RunnerLayoutUi layoutUi = factory.create("", "", "session", myProject);
+
+    Content console = layoutUi.createContent(BlazeConsoleToolWindowFactory.ID, myConsoleView.getComponent(), "", null, null);
+    layoutUi.addContent(console, 0, PlaceInGrid.right, false);
+
+    // Adding actions
+    DefaultActionGroup group = new DefaultActionGroup();
+    layoutUi.getOptions().setLeftToolbar(group, ActionPlaces.UNKNOWN);
+
+    AnAction[] consoleActions = myConsoleView.createConsoleActions();
+    for (AnAction action : consoleActions) {
+      if (!shouldIgnoreAction(action)) {
+        group.add(action);
+      }
+    }
+    group.add(new StopAction());
+
+    JComponent layoutComponent = layoutUi.getComponent();
+    myConsolePanel.add(layoutComponent, BorderLayout.CENTER);
+
+    //noinspection ConstantConditions
+    Content content =
+      ContentFactory.SERVICE.getInstance().createContent(layoutComponent, null, true);
+    toolWindow.getContentManager().addContent(content);
+  }
+
+  public void clear() {
+    myConsoleView.clear();
+  }
+
+  public void print(@NotNull String text, @NotNull ConsoleViewContentType contentType) {
+    myConsoleView.print(text, contentType);
+  }
+
+  @Override
+  public void dispose() {
+  }
+
+  private void setupUI() {
+    myConsolePanel = new JPanel();
+    myConsolePanel.setLayout(new BorderLayout(0, 0));
+  }
+
+  private class StopAction extends DumbAwareAction {
+    public StopAction() {
+      super(IdeBundle.message("action.stop"), null, AllIcons.Actions.Suspend);
+    }
+
+    @Override
+    public void actionPerformed(AnActionEvent e) {
+      Runnable handler = myStopHandler;
+      if (handler != null) {
+        handler.run();
+        myStopHandler = null;
+      }
+    }
+
+    @Override
+    public void update(AnActionEvent event) {
+      Presentation presentation = event.getPresentation();
+      boolean isNowVisible = myStopHandler != null;
+      if (presentation.isEnabled() != isNowVisible) {
+        presentation.setEnabled(isNowVisible);
+      }
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/BoolExperiment.java b/blaze-base/src/com/google/idea/blaze/base/experiments/BoolExperiment.java
new file mode 100644
index 0000000..98da296
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/BoolExperiment.java
@@ -0,0 +1,34 @@
+/*
+ * 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.experiments;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Boolean-valued experiment.
+ */
+public class BoolExperiment extends Experiment {
+  private final boolean defaultValue;
+
+  public BoolExperiment(@NotNull String key, boolean defaultValue) {
+    super(key);
+    this.defaultValue = defaultValue;
+  }
+
+  public boolean getValue() {
+    return ExperimentService.getInstance().getExperiment(key, defaultValue);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/DevOverride.java b/blaze-base/src/com/google/idea/blaze/base/experiments/DevOverride.java
new file mode 100644
index 0000000..d990178
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/DevOverride.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.experiments;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Developer override for string experiments.
+ */
+public class DevOverride extends Experiment {
+  private final String defaultValue;
+
+  public DevOverride(@NotNull String key, @NotNull String defaultValue) {
+    super(key);
+    this.defaultValue = defaultValue;
+  }
+
+  @NotNull
+  public String getValue() {
+    return ExperimentService.getInstance().getExperimentString(key, defaultValue);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/DeveloperFlag.java b/blaze-base/src/com/google/idea/blaze/base/experiments/DeveloperFlag.java
new file mode 100644
index 0000000..6813bde
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/DeveloperFlag.java
@@ -0,0 +1,27 @@
+/*
+ * 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.experiments;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This is an experiment that always defaults to off. Use to document that this is for devs only.
+ */
+public class DeveloperFlag extends BoolExperiment {
+  public DeveloperFlag(@NotNull String key) {
+    super(key, false);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/Experiment.java b/blaze-base/src/com/google/idea/blaze/base/experiments/Experiment.java
new file mode 100644
index 0000000..a320391
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/Experiment.java
@@ -0,0 +1,34 @@
+/*
+ * 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.experiments;
+
+import com.intellij.openapi.components.ServiceManager;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Experiment class.
+ */
+public abstract class Experiment {
+  @NotNull protected final String key;
+
+  protected Experiment(@NotNull String key) {
+    this.key = key;
+  }
+
+  public String getKey() {
+    return key;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentLoader.java b/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentLoader.java
new file mode 100644
index 0000000..b9450c8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentLoader.java
@@ -0,0 +1,24 @@
+/*
+ * 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.experiments;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+
+interface ExperimentLoader {
+  Map<String, String> getExperiments();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentScope.java b/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentScope.java
new file mode 100644
index 0000000..3787678
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentScope.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.experiments;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Reloads experiments at the start of the scope.
+ */
+public class ExperimentScope implements BlazeScope {
+  @Override
+  public void onScopeBegin(@NotNull BlazeContext context) {
+    ExperimentService.getInstance().reloadExperiments();
+    ExperimentService.getInstance().startExperimentScope();
+  }
+
+  @Override
+  public void onScopeEnd(@NotNull BlazeContext context) {
+    ExperimentService.getInstance().endExperimentScope();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentService.java b/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentService.java
new file mode 100644
index 0000000..4a80be3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentService.java
@@ -0,0 +1,61 @@
+/*
+ * 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.experiments;
+
+import com.intellij.openapi.components.ServiceManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Reads experiments.
+ */
+public interface ExperimentService {
+
+  static ExperimentService getInstance() {
+    return ServiceManager.getService(ExperimentService.class);
+  }
+
+  /**
+   * Returns an experiment if it exists, else defaultValue
+   */
+  boolean getExperiment(@NotNull String key, boolean defaultValue);
+
+  /**
+   * Returns a string-valued experiment if it exists, else defaultValue.
+   */
+  String getExperimentString(@NotNull String key, @Nullable String defaultValue);
+
+  /**
+   * Returns an int-valued experiment if it exists, else defaultValue.
+   */
+  int getExperimentInt(@NotNull String key, int defaultValue);
+
+  /**
+   * Reloads all experiments.
+   */
+  void reloadExperiments();
+
+  /**
+   * Starts an experiment scope. During an experiment scope,
+   * experiments won't be reloaded.
+   */
+  void startExperimentScope();
+
+  /**
+   * Ends an experiment scope.
+   */
+  void endExperimentScope();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentServiceImpl.java b/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentServiceImpl.java
new file mode 100644
index 0000000..2c2911d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/ExperimentServiceImpl.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.base.experiments;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.util.SystemProperties;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An experiment service that delegates to {@link ExperimentLoader ExperimentLoaders}, in a
+ * specific order.
+ *
+ * It will check system properties first, then an experiment file in the user's home directory, then
+ * finally all files specified by the system property blaze.experiments.file.
+ */
+public class ExperimentServiceImpl implements ExperimentService {
+  private static final Logger LOG = Logger.getInstance(ExperimentServiceImpl.class);
+
+  private static final String USER_EXPERIMENT_OVERRIDES_FILE =
+      SystemProperties.getUserHome() + File.separator + ".blaze-experiments";
+
+  private final List<ExperimentLoader> services;
+  private Map<String, String> experiments;
+  private AtomicInteger experimentScopeCounter = new AtomicInteger(0);
+
+  public ExperimentServiceImpl() {
+    this(
+        new SystemPropertyExperimentLoader(),
+        new FileExperimentLoader(USER_EXPERIMENT_OVERRIDES_FILE),
+        new WebExperimentLoader());
+  }
+
+  @VisibleForTesting
+  protected ExperimentServiceImpl(ExperimentLoader... loaders) {
+    services = ImmutableList.copyOf(loaders);
+  }
+
+  @Override
+  public synchronized void reloadExperiments() {
+    if (experimentScopeCounter.get() > 0) {
+      return;
+    }
+
+    Map<String, String> experiments = Maps.newHashMap();
+    for (ExperimentLoader loader : Lists.reverse(services)) {
+      experiments.putAll(loader.getExperiments());
+    }
+    this.experiments = experiments;
+  }
+
+  @Override
+  public synchronized void startExperimentScope() {
+    this.experimentScopeCounter.incrementAndGet();
+  }
+
+  @Override
+  public synchronized void endExperimentScope() {
+    int value = this.experimentScopeCounter.decrementAndGet();
+    LOG.assertTrue(value >= 0);
+  }
+
+  @Override
+  public boolean getExperiment(@NotNull String key, boolean defaultValue) {
+    String property = getExperiment(key);
+    return property != null ? property.equals("1") : defaultValue;
+  }
+
+  @Override
+  public String getExperimentString(@NotNull String key, String defaultValue) {
+    String property = getExperiment(key);
+    return property != null ? property : defaultValue;
+  }
+
+  @Override
+  public int getExperimentInt(@NotNull String key, int defaultValue) {
+    String property = getExperiment(key);
+    try {
+      return property != null ? Integer.parseInt(property) : defaultValue;
+    } catch (NumberFormatException e) {
+      LOG.warn("Could not parse int for experiment: " + key, e);
+      return defaultValue;
+    }
+  }
+
+  String getExperiment(@NotNull String key) {
+    if (experiments == null) {
+      reloadExperiments();
+    }
+    LOG.assertTrue(experiments != null, "Failure to load experiments.");
+    return experiments.get(key);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/FileExperimentLoader.java b/blaze-base/src/com/google/idea/blaze/base/experiments/FileExperimentLoader.java
new file mode 100644
index 0000000..07d5f55
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/FileExperimentLoader.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.experiments;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.intellij.openapi.diagnostic.Logger;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Reads experiments from a property file.
+ */
+class FileExperimentLoader implements ExperimentLoader {
+
+  private static final Logger LOG = Logger.getInstance(FileExperimentLoader.class);
+
+  private final String filename;
+
+  public FileExperimentLoader(String filename) {
+    this.filename = filename;
+  }
+
+  @Override
+  public Map<String, String> getExperiments() {
+    Properties properties = new Properties();
+
+    File file = new File(filename);
+    if (!file.exists()) {
+      LOG.info("File " + filename + " does not exist, skipping.");
+      return ImmutableMap.of();
+    }
+
+    Map<String, String> result = Maps.newHashMap();
+    try (InputStream inputStream = new FileInputStream(filename)) {
+      properties.load(inputStream);
+      Enumeration<?> enumeration = properties.propertyNames();
+      while (enumeration.hasMoreElements()) {
+        String key = (String)enumeration.nextElement();
+        String value = properties.getProperty(key);
+        result.put(key, value);
+      }
+    } catch (IOException e) {
+      LOG.warn("Could not load experiments from file: " + filename, e);
+    }
+    return result;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/IntExperiment.java b/blaze-base/src/com/google/idea/blaze/base/experiments/IntExperiment.java
new file mode 100644
index 0000000..412e6f1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/IntExperiment.java
@@ -0,0 +1,34 @@
+/*
+ * 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.experiments;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Integer valued experiment.
+ */
+public class IntExperiment extends Experiment {
+  private final int defaultValue;
+
+  public IntExperiment(@NotNull String key, int defaultValue) {
+    super(key);
+    this.defaultValue = defaultValue;
+  }
+
+  public int getValue() {
+    return ExperimentService.getInstance().getExperimentInt(key, defaultValue);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/StringExperiment.java b/blaze-base/src/com/google/idea/blaze/base/experiments/StringExperiment.java
new file mode 100644
index 0000000..b881bf1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/StringExperiment.java
@@ -0,0 +1,34 @@
+/*
+ * 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.experiments;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * String-valued experiment.
+ */
+public class StringExperiment extends Experiment {
+
+  public StringExperiment(@NotNull String key) {
+    super(key);
+  }
+
+  @Nullable
+  public String getValue() {
+    return ExperimentService.getInstance().getExperimentString(key, null);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/SystemPropertyExperimentLoader.java b/blaze-base/src/com/google/idea/blaze/base/experiments/SystemPropertyExperimentLoader.java
new file mode 100644
index 0000000..8a02670
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/SystemPropertyExperimentLoader.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.experiments;
+
+import com.google.common.collect.ImmutableMap;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+
+class SystemPropertyExperimentLoader implements ExperimentLoader {
+  private static final String BLAZE_EXPERIMENT_OVERRIDE = "blaze.experiment.";
+
+  @Override
+  public Map<String, String> getExperiments() {
+    // Cache the current values of the experiments so that they don't change in the
+    // current ExperimentScope.
+    ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
+    System.getProperties()
+        .stringPropertyNames()
+        .stream()
+        .filter(name -> name.startsWith(BLAZE_EXPERIMENT_OVERRIDE))
+        .forEach(
+            name ->
+                mapBuilder.put(
+                    name.substring(BLAZE_EXPERIMENT_OVERRIDE.length()), System.getProperty(name)));
+    return mapBuilder.build();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/WebExperimentLoader.java b/blaze-base/src/com/google/idea/blaze/base/experiments/WebExperimentLoader.java
new file mode 100644
index 0000000..67e51f3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/WebExperimentLoader.java
@@ -0,0 +1,28 @@
+/*
+ * 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.experiments;
+
+import com.google.common.collect.ImmutableMap;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+
+class WebExperimentLoader implements ExperimentLoader {
+  @Override
+  public Map<String, String> getExperiments() {
+    return WebExperimentSyncer.getInstance().getExperimentValues();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/experiments/WebExperimentSyncer.java b/blaze-base/src/com/google/idea/blaze/base/experiments/WebExperimentSyncer.java
new file mode 100644
index 0000000..e4675f9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/experiments/WebExperimentSyncer.java
@@ -0,0 +1,196 @@
+/*
+ * 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.experiments;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListenableScheduledFuture;
+import com.google.common.util.concurrent.ListeningScheduledExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.gson.JsonParseException;
+import com.google.idea.blaze.base.util.SerializationUtil;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.util.io.HttpRequests;
+import org.jetbrains.io.JsonReaderEx;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.*;
+
+/**
+ * A singleton class that retrieves the experiments from the experiments service</a>.
+ *
+ * The first time {@link #getExperimentValues()} is called, fresh data will be retrieved in the
+ * current thread. Thereafter, data will be retrieved every 5 minutes in a background thread. If
+ * there is a failure retrieving data, new attempts will be made every minute.
+ */
+class WebExperimentSyncer {
+  private static final String DEFAULT_EXPERIMENT_URL =
+      "https://intellij-experiments.appspot.com/api/experiments";
+  private static final String EXPERIMENTS_URL_PROPERTY = "blaze.experiments.url";
+
+  private static final int SUCESSFUL_DOWNLOAD_DELAY_MINUTES = 5;
+  private static final int DOWNLOAD_FAILURE_DELAY_MINUTES = 1;
+
+  private static final String CACHE_FILE_NAME = "blaze.experiments.cache.dat";
+
+  private static final Logger LOG = Logger.getInstance(WebExperimentSyncer.class);
+
+  private static final WebExperimentSyncer INSTANCE = new WebExperimentSyncer();
+
+  // null indicates no fetch attempt has been made. After the first attempt, this will always be a
+  // (possibly empty) map.
+  private Map<String, String> experimentValues = null;
+
+  private ListeningScheduledExecutorService executor =
+      MoreExecutors.listeningDecorator(
+          MoreExecutors.getExitingScheduledExecutorService(
+              new ScheduledThreadPoolExecutor(1), 0, TimeUnit.SECONDS));
+
+  private WebExperimentSyncer() {}
+
+  public static WebExperimentSyncer getInstance() {
+    return INSTANCE;
+  }
+
+  /**
+   * Get the last-retrieved set of experiment values.
+   *
+   * The first time this method is called, an attempt to retrieve the values will take place.
+   * Thereafter, the values will be retrieved every five minutes on a background thread and this
+   * method will return the most recent successfully retrieved values.
+   */
+  public synchronized Map<String, String> getExperimentValues() {
+    if (experimentValues == null) {
+      initialize();
+    }
+
+    return experimentValues;
+  }
+
+  private synchronized void setExperimentValues(HashMap<String, String> experimentValues) {
+    this.experimentValues = experimentValues;
+    saveCache(experimentValues);
+  }
+
+  /**
+   * Fetch and process the experiments on the current thread.
+   */
+  private void initialize() {
+    ListenableFuture<String> response =
+        MoreExecutors.sameThreadExecutor().submit(new WebExperimentsDownloader());
+    response.addListener(
+        new WebExperimentsResultProcessor(response, false), MoreExecutors.sameThreadExecutor());
+
+    // Failed to fetch, try to load cache from disk
+    if (experimentValues == null) {
+      experimentValues = loadCache();
+    }
+
+    // There must have been an error retrieving the experiments.
+    if (experimentValues == null) {
+      experimentValues = ImmutableMap.of();
+    }
+  }
+
+  private void scheduleNextRefresh(boolean refreshWasSuccessful) {
+    int delayInMinutes =
+        refreshWasSuccessful ? SUCESSFUL_DOWNLOAD_DELAY_MINUTES : DOWNLOAD_FAILURE_DELAY_MINUTES;
+    ListenableScheduledFuture<String> refreshResults =
+        executor.schedule(new WebExperimentsDownloader(), delayInMinutes, TimeUnit.MINUTES);
+    refreshResults.addListener(
+        new WebExperimentsResultProcessor(refreshResults, true), MoreExecutors.sameThreadExecutor());
+  }
+
+  private static class WebExperimentsDownloader implements Callable<String> {
+
+    @Override
+    public String call() throws Exception {
+      LOG.debug("About to fetch experiments.");
+      return HttpRequests.request(
+              System.getProperty(EXPERIMENTS_URL_PROPERTY, DEFAULT_EXPERIMENT_URL))
+          .readString(null /* progress indicator */);
+    }
+  }
+
+  private class WebExperimentsResultProcessor implements Runnable {
+
+    private final Future<String> resultFuture;
+    private final boolean triggerExperimentsReload;
+
+    private WebExperimentsResultProcessor(Future<String> resultFuture,
+                                          boolean triggerExperimentsReload) {
+      this.resultFuture = resultFuture;
+      this.triggerExperimentsReload = triggerExperimentsReload;
+    }
+
+    @Override
+    public void run() {
+      LOG.debug("Experiments fetched. Processing results.");
+      try {
+        HashMap<String, String> mapBuilder = Maps.newHashMap();
+        String result = resultFuture.get();
+        try (JsonReaderEx reader = new JsonReaderEx(result)) {
+          reader.beginObject();
+          while (reader.hasNext()) {
+            String experimentName = reader.nextName();
+            String experimentValue = reader.nextString();
+            mapBuilder.put(experimentName, experimentValue);
+          }
+        }
+        setExperimentValues(mapBuilder);
+
+        if (triggerExperimentsReload) {
+          ExperimentService.getInstance().reloadExperiments();
+        }
+        LOG.debug("Successfully fetched experiments: " + getExperimentValues());
+        scheduleNextRefresh(true /* refreshWasSuccessful */);
+      } catch (InterruptedException | ExecutionException | JsonParseException e) {
+        LOG.debug("Error fetching experiments", e);
+        scheduleNextRefresh(false /* refreshWasSuccessful */);
+      }
+    }
+  }
+
+  private static void saveCache(HashMap<String, String> experiments) {
+    try {
+      SerializationUtil.saveToDisk(getCacheFile(), experiments);
+    } catch (IOException e) {
+      LOG.warn("Could not save experiments cache to disk: " + getCacheFile());
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static HashMap<String, String> loadCache() {
+    try {
+      return (HashMap<String, String>)SerializationUtil.loadFromDisk(getCacheFile(), ImmutableList.of());
+    }
+    catch (IOException e) {
+      // This is normal, we might be offline and have never loaded the cache.
+      LOG.info("Could not load experiments file: " + getCacheFile());
+    }
+    return null;
+  }
+
+  private static File getCacheFile() {
+    return new File(new File(PathManager.getSystemPath(), "blaze"), CACHE_FILE_NAME).getAbsoluteFile();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/help/BlazeHelpHandler.java b/blaze-base/src/com/google/idea/blaze/base/help/BlazeHelpHandler.java
new file mode 100644
index 0000000..145748c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/help/BlazeHelpHandler.java
@@ -0,0 +1,29 @@
+/*
+ * 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.help;
+
+import com.intellij.openapi.components.ServiceManager;
+
+/**
+ * Handles help requests.
+ */
+public interface BlazeHelpHandler {
+  static BlazeHelpHandler getInstance() {
+    return ServiceManager.getService(BlazeHelpHandler.class);
+  }
+
+  void handleHelp(String urlFragment);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java b/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java
new file mode 100644
index 0000000..3471e5f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazePackageAction.java
@@ -0,0 +1,236 @@
+/*
+ * 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.ide;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.buildmodifier.BuildFileModifier;
+import com.google.idea.blaze.base.buildmodifier.FileSystemModifier;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.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.Scope;
+import com.google.idea.blaze.base.scope.ScopedOperation;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.intellij.history.LocalHistory;
+import com.intellij.history.LocalHistoryAction;
+import com.intellij.ide.IdeView;
+import com.intellij.ide.util.DirectoryChooserUtil;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class NewBlazePackageAction extends BlazeAction implements DumbAware {
+  private static final Logger LOG = Logger.getInstance(NewBlazePackageAction.class);
+
+  private static final String BUILD_FILE_NAME = "BUILD";
+
+  public NewBlazePackageAction() {
+    super();
+  }
+
+  @Override
+  public void actionPerformed(AnActionEvent event) {
+    final IdeView view = event.getData(LangDataKeys.IDE_VIEW);
+    final Project project = event.getData(CommonDataKeys.PROJECT);
+    Scope.root(new ScopedOperation() {
+      @Override
+      public void execute(@NotNull final BlazeContext context) {
+        context.push(new LoggedTimingScope(project, Action.CREATE_BLAZE_PACKAGE));
+
+        if (view == null || project == null) {
+          return;
+        }
+        PsiDirectory directory = getOrChooseDirectory(project, view);
+
+        if (directory == null) {
+          return;
+        }
+
+        NewBlazePackageDialog newBlazePackageDialog = new NewBlazePackageDialog(project, directory);
+        boolean isOk = newBlazePackageDialog.showAndGet();
+        if (!isOk) {
+          return;
+        }
+
+        final Label newRule = newBlazePackageDialog.getNewRule();
+        final Kind newRuleKind = newBlazePackageDialog.getNewRuleKind();
+        // If we returned OK, we should have a non null result
+        LOG.assertTrue(newRule != null);
+        LOG.assertTrue(newRuleKind != null);
+
+        context.output(new StatusOutput(String.format("Setting up a new %s package", Blaze.buildSystemName(project))));
+
+        boolean success = createPackageOnDisk(
+          project,
+          context,
+          newRule,
+          newRuleKind
+        );
+
+        if (!success) {
+          return;
+        }
+
+        File newDirectory = WorkspaceRoot.fromProject(project).fileForPath(newRule.blazePackage());
+        VirtualFile virtualFile = VfsUtil.findFileByIoFile(newDirectory, true);
+        // We just created this file, it should exist
+        LOG.assertTrue(virtualFile != null);
+        PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile);
+        view.selectElement(psiFile);
+      }
+    });
+  }
+
+  private static Boolean createPackageOnDisk(
+    @NotNull Project project,
+    @NotNull BlazeContext context,
+    @NotNull Label newRule,
+    @NotNull Kind ruleKind) {
+    LocalHistoryAction action;
+
+    String actionName = String.format("Creating %s package: %s", Blaze.buildSystemName(project), newRule.toString());
+    LocalHistory localHistory = LocalHistory.getInstance();
+    action = localHistory.startAction(actionName);
+
+    // Create the package + BUILD file + rule
+    FileSystemModifier fileSystemModifier = FileSystemModifier.getInstance(project);
+    WorkspacePath newWorkspacePath = newRule.blazePackage();
+    File newDirectory = fileSystemModifier.makeWorkspacePathDirs(newWorkspacePath);
+    if (newDirectory == null) {
+      String errorMessage = "Could not create new package directory: " + newWorkspacePath.toString();
+      context.output(PrintOutput.error(errorMessage));
+      return false;
+    }
+    File buildFile = fileSystemModifier.createFile(newWorkspacePath, BUILD_FILE_NAME);
+    if (buildFile == null) {
+      String errorMessage =
+        "Could not create new BUILD file in package: " + newWorkspacePath.toString();
+      context.output(PrintOutput.error(errorMessage));
+      return false;
+    }
+    BuildFileModifier buildFileModifier = BuildFileModifier.getInstance();
+    buildFileModifier.addRule(project, context, newRule, ruleKind);
+    action.finish();
+
+    return true;
+  }
+
+  @Override
+  protected void doUpdate(@NotNull AnActionEvent event) {
+    Presentation presentation = event.getPresentation();
+    if (isEnabled(event)) {
+      String text = String.format("New %s Package", Blaze.buildSystemName(event.getProject()));
+      presentation.setEnabledAndVisible(true);
+      presentation.setText(text);
+      presentation.setDescription(text);
+      presentation.setIcon(PlatformIcons.PACKAGE_ICON);
+    } else {
+      presentation.setEnabledAndVisible(false);
+    }
+  }
+
+  private boolean isEnabled(AnActionEvent event) {
+    Project project = event.getProject();
+    IdeView view = event.getData(LangDataKeys.IDE_VIEW);
+    if (project == null || view == null) {
+      return false;
+    }
+
+    List<PsiDirectory> directories = filterDirectories(project, view.getDirectories());
+    if (directories.isEmpty()) {
+      return false;
+    }
+
+    return true;
+  }
+
+  /**
+   * Filter out directories that do not live under the project's directories.
+   */
+  private static List<PsiDirectory> filterDirectories(Project project, PsiDirectory[] directories) {
+    if (directories.length == 0) {
+      return ImmutableList.of();
+    }
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet == null) {
+      return ImmutableList.of();
+    }
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    ImportRoots importRoots = ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project)).add(projectViewSet).build();
+    return Lists.newArrayList(directories).stream()
+      .filter(directory -> isUnderProjectViewDirectory(workspaceRoot, importRoots, directory))
+      .collect(Collectors.toList());
+  }
+
+  private static boolean isUnderProjectViewDirectory(WorkspaceRoot workspaceRoot,
+                                                     ImportRoots importRoots,
+                                                     PsiDirectory directory) {
+    VirtualFile virtualFile = directory.getVirtualFile();
+    // Ignore jars, etc. and their contents, which are in an ArchiveFileSystem.
+    if (!(virtualFile.isInLocalFileSystem())) {
+      return false;
+    }
+    if (!workspaceRoot.isInWorkspace(virtualFile)) {
+      return false;
+    }
+    WorkspacePath workspacePath = workspaceRoot.workspacePathFor(virtualFile);
+    return importRoots.rootDirectories().stream()
+      .anyMatch(importRoot -> FileUtil.isAncestor(importRoot.relativePath(), workspacePath.relativePath(), false));
+  }
+
+  @Nullable
+  private static PsiDirectory getOrChooseDirectory(Project project, IdeView view) {
+    List<PsiDirectory> dirs = filterDirectories(project, view.getDirectories());
+    if (dirs.size() == 0) {
+      return null;
+    }
+    if (dirs.size() == 1) {
+      return dirs.get(0);
+    }
+    else {
+      return DirectoryChooserUtil.selectDirectory(project, dirs.toArray(new PsiDirectory[dirs.size()]), null, "");
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java b/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
new file mode 100644
index 0000000..d99d254
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazePackageDialog.java
@@ -0,0 +1,144 @@
+/*
+ * 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.ide;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.*;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.CommonBundle;
+import com.intellij.ide.IdeBundle;
+import com.intellij.ide.actions.CreateElementActionBase;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBTextField;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.util.List;
+
+public class NewBlazePackageDialog extends DialogWrapper {
+  private static final Logger LOG = Logger.getInstance(NewBlazePackageDialog.class);
+
+  @NotNull private final Project project;
+  @NotNull private final PsiDirectory parentDirectory;
+
+  @Nullable private Label newRule;
+  @Nullable private Kind newRuleKind;
+
+  private static final int UI_INDENT_LEVEL = 0;
+  private static final int TEXT_FIELD_LENGTH = 40;
+  @NotNull private final JPanel component = new JPanel(new GridBagLayout());
+  @NotNull private final JBLabel packageLabel = new JBLabel("Package name:");
+  @NotNull private final JBTextField packageNameField = new JBTextField(TEXT_FIELD_LENGTH);
+  @NotNull private final NewRuleUI newRuleUI = new NewRuleUI(TEXT_FIELD_LENGTH);
+
+  public NewBlazePackageDialog(@NotNull Project project,
+                               @NotNull PsiDirectory currentDirectory) {
+    super(project);
+    this.project = project;
+    this.parentDirectory = currentDirectory;
+
+    initializeUI();
+  }
+
+  private void initializeUI() {
+    component.add(packageLabel);
+    component.add(packageNameField, UiUtil.getFillLineConstraints(UI_INDENT_LEVEL));
+    newRuleUI.fillUI(component, UI_INDENT_LEVEL);
+    UiUtil.fillBottom(component);
+    init();
+  }
+
+  @Nullable
+  @Override
+  protected JComponent createCenterPanel() {
+    return component;
+  }
+
+  @Nullable
+  @Override
+  protected ValidationInfo doValidate() {
+    String packageName = packageNameField.getText();
+    if (packageName == null) {
+      return new ValidationInfo("Internal error, package was null");
+    }
+    if (packageName.length() == 0) {
+      return new ValidationInfo(
+        IdeBundle.message("error.name.should.be.specified"),
+        packageNameField
+      );
+    }
+    List<BlazeValidationError> errors = Lists.newArrayList();
+    if (!Label.validatePackagePath(packageName, errors)) {
+      BlazeValidationError validationResult = errors.get(0);
+      return new ValidationInfo(validationResult.getError(), packageNameField);
+    }
+
+    return newRuleUI.validate();
+  }
+
+  @Override
+  protected void doOKAction() {
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    LOG.assertTrue(parentDirectory.getVirtualFile().isInLocalFileSystem());
+    File parentDirectoryFile = new File(parentDirectory.getVirtualFile().getPath());
+    String newPackageName = packageNameField.getText();
+    File newPackageDirectory = new File(parentDirectoryFile, newPackageName);
+    WorkspacePath newPackagePath = workspaceRoot.workspacePathFor(newPackageDirectory);
+
+    RuleName newRuleName = newRuleUI.getRuleName();
+    Label newRule = new Label(newPackagePath, newRuleName);
+    Kind ruleKind = newRuleUI.getSelectedRuleKind();
+    try {
+      parentDirectory.checkCreateSubdirectory(newPackageName);
+    }
+    catch (IncorrectOperationException ex) {
+      showErrorDialog(CreateElementActionBase.filterMessage(ex.getMessage()));
+      // do not close the dialog
+      return;
+    }
+    this.newRule = newRule;
+    this.newRuleKind = ruleKind;
+    super.doOKAction();
+  }
+
+  private void showErrorDialog(@NotNull String message) {
+    String title = CommonBundle.getErrorTitle();
+    Icon icon = Messages.getErrorIcon();
+    Messages.showMessageDialog(component, message, title, icon);
+  }
+
+  @Nullable
+  public Label getNewRule() {
+    return newRule;
+  }
+
+  @Nullable
+  public Kind getNewRuleKind() {
+    return newRuleKind;
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java b/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java
new file mode 100644
index 0000000..4efe8d6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazeRuleAction.java
@@ -0,0 +1,85 @@
+/*
+ * 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.ide;
+
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.experiments.ExperimentScope;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.ScopedOperation;
+import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.actionSystem.*;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+
+public class NewBlazeRuleAction extends BlazeAction implements DumbAware {
+
+  public NewBlazeRuleAction() {
+    super();
+  }
+
+  @Override
+  public void actionPerformed(AnActionEvent event) {
+    final Project project = event.getData(CommonDataKeys.PROJECT);
+    if (project == null) {
+      return;
+    }
+    final VirtualFile virtualFile = event.getData(CommonDataKeys.VIRTUAL_FILE);
+    if (virtualFile == null) {
+      return;
+    }
+
+    Scope.root(new ScopedOperation() {
+      @Override
+      public void execute(@NotNull BlazeContext context) {
+        context
+          .push(new ExperimentScope())
+          .push(new BlazeConsoleScope.Builder(project).build())
+          .push(new IssuesScope(project))
+          .push(new LoggedTimingScope(project, Action.CREATE_BLAZE_RULE))
+        ;
+        NewBlazeRuleDialog newBlazeRuleDialog = new NewBlazeRuleDialog(context, project, virtualFile);
+        newBlazeRuleDialog.show();
+      }
+    });
+  }
+
+  @Override
+  protected void doUpdate(@NotNull AnActionEvent event) {
+    Presentation presentation = event.getPresentation();
+    DataContext dataContext = event.getDataContext();
+    VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(dataContext);
+    Project project = CommonDataKeys.PROJECT.getData(dataContext);
+    boolean enabled = (project != null && file != null && file.getName().equals("BUILD"));
+    presentation.setVisible(enabled || ActionPlaces.isMainMenuOrActionSearch(event.getPlace()));
+    presentation.setEnabled(enabled);
+    presentation.setText(getText(project));
+  }
+
+  private static String getText(@Nullable Project project) {
+    String buildSystem = Blaze.buildSystemName(project);
+    return String.format("New %s Rule", buildSystem);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java b/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java
new file mode 100644
index 0000000..5cfc981
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ide/NewBlazeRuleDialog.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ide;
+
+import com.google.idea.blaze.base.buildmodifier.BuildFileModifier;
+import com.google.idea.blaze.base.model.primitives.*;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.DialogWrapper;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+
+public class NewBlazeRuleDialog extends DialogWrapper {
+  private static final Logger LOG = Logger.getInstance(NewBlazeRuleDialog.class);
+
+  private static final int UI_INDENT = 0;
+  private static final int TEXT_BOX_WIDTH = 40;
+
+  private final BlazeContext context;
+  private final Project project;
+  private final VirtualFile buildFile;
+  private final String buildSystemName;
+
+  private JPanel component = new JPanel(new GridBagLayout());
+  private final NewRuleUI newRuleUI = new NewRuleUI(TEXT_BOX_WIDTH);
+  private static final Dimension componentSize = new Dimension(500, 500);
+
+  public NewBlazeRuleDialog(BlazeContext context,
+                            Project project,
+                            VirtualFile buildFile) {
+    super(project);
+    this.context = context;
+    this.project = project;
+    this.buildFile = buildFile;
+    this.buildSystemName = Blaze.buildSystemName(project);
+    initComponent();
+  }
+
+  private void initComponent() {
+    setTitle(String.format("Create a New %s Rule", buildSystemName));
+    setOKButtonText("Create");
+    setCancelButtonText("Cancel");
+
+    component.setPreferredSize(componentSize);
+    component.setMinimumSize(componentSize);
+
+    newRuleUI.fillUI(component, UI_INDENT);
+    UiUtil.fillBottom(component);
+
+    init();
+  }
+
+  @Nullable
+  @Override
+  protected JComponent createCenterPanel() {
+    return component;
+  }
+
+  @Nullable
+  @Override
+  protected ValidationInfo doValidate() {
+    return newRuleUI.validate();
+  }
+
+  @Override
+  protected void doOKAction() {
+    RuleName ruleName = newRuleUI.getRuleName();
+    Kind ruleKind = newRuleUI.getSelectedRuleKind();
+
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    WorkspacePath workspacePath = workspaceRoot.workspacePathFor(new File(buildFile.getParent().getPath()));
+    Label newRule = new Label(workspacePath, ruleName);
+    BuildFileModifier buildFileModifier = BuildFileModifier.getInstance();
+    boolean success = buildFileModifier.addRule(project, context, newRule, ruleKind);
+
+    if (success) {
+      super.doOKAction();
+    }
+    else {
+      super.setErrorText(String.format("Could not create new rule, see %s Console for details", buildSystemName));
+    }
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ide/NewRuleUI.java b/blaze-base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
new file mode 100644
index 0000000..de090d9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ide/NewRuleUI.java
@@ -0,0 +1,88 @@
+/*
+ * 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.ide;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.RuleName;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.ide.IdeBundle;
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.openapi.ui.ValidationInfo;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.ui.components.JBTextField;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.util.Collection;
+import java.util.List;
+
+final class NewRuleUI {
+
+  private static final String[] POSSIBLE_RULES = {
+    "android_library",
+    "java_library",
+    "cc_library",
+    "cc_binary",
+    "proto_library"
+  };
+
+  @NotNull private final ComboBox ruleComboBox = new ComboBox(POSSIBLE_RULES);
+  @NotNull private final JBLabel ruleNameLabel = new JBLabel("Rule name:");
+  @NotNull private final JBTextField ruleNameField;
+
+  public NewRuleUI(int textFieldLength) {
+    this.ruleNameField = new JBTextField(textFieldLength);
+  }
+
+  public void fillUI(@NotNull JPanel component, int indentLevel) {
+    component.add(ruleNameLabel);
+    component.add(ruleNameField, UiUtil.getFillLineConstraints(indentLevel));
+    component.add(ruleComboBox, UiUtil.getFillLineConstraints(indentLevel));
+  }
+
+  @NotNull
+  public Kind getSelectedRuleKind() {
+    return Kind.fromString((String)ruleComboBox.getSelectedItem());
+  }
+
+  @NotNull
+  public RuleName getRuleName() {
+    return RuleName.create(ruleNameField.getText());
+  }
+
+  @Nullable
+  public ValidationInfo validate() {
+    String ruleName = ruleNameField.getText();
+    List<BlazeValidationError> errors = Lists.newArrayList();
+    if (!validateRuleName(ruleName, errors)) {
+      BlazeValidationError issue = errors.get(0);
+      return new ValidationInfo(issue.getError(), ruleNameField);
+    }
+    return null;
+  }
+
+  private static boolean validateRuleName(@NotNull String inputString, @Nullable Collection<BlazeValidationError> errors) {
+    if (inputString.length() == 0) {
+      BlazeValidationError.collect(errors, new BlazeValidationError(IdeBundle.message("error.name.should.be.specified")));
+      return false;
+    }
+
+    return RuleName.validate(inputString, errors);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/AndroidRuleIdeInfo.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/AndroidRuleIdeInfo.java
new file mode 100644
index 0000000..fa567a5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/AndroidRuleIdeInfo.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.Label;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Ide info specific to android rules.
+ */
+public final class AndroidRuleIdeInfo implements Serializable {
+  private static final long serialVersionUID = 5L;
+
+  public final Collection<ArtifactLocation> resources;
+  @Nullable public final ArtifactLocation manifest;
+  @Nullable public final LibraryArtifact idlJar;
+  @Nullable public final LibraryArtifact resourceJar;
+  public final boolean hasIdlSources;
+  @Nullable public final String resourceJavaPackage;
+  public boolean generateResourceClass;
+  @Nullable public Label legacyResources;
+
+  public AndroidRuleIdeInfo(Collection<ArtifactLocation> resources,
+                            @Nullable String resourceJavaPackage,
+                            boolean generateResourceClass,
+                            @Nullable ArtifactLocation manifest,
+                            @Nullable LibraryArtifact idlJar,
+                            @Nullable LibraryArtifact resourceJar,
+                            boolean hasIdlSources,
+                            @Nullable Label legacyResources) {
+    this.resources = resources;
+    this.resourceJavaPackage = resourceJavaPackage;
+    this.generateResourceClass = generateResourceClass;
+    this.manifest = manifest;
+    this.idlJar = idlJar;
+    this.resourceJar = resourceJar;
+    this.hasIdlSources = hasIdlSources;
+    this.legacyResources = legacyResources;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private Collection<ArtifactLocation> resources = Lists.newArrayList();
+    private ArtifactLocation manifest;
+    private LibraryArtifact idlJar;
+    private LibraryArtifact resourceJar;
+    private boolean hasIdlSources;
+    private String resourceJavaPackage;
+    private boolean generateResourceClass;
+    private Label legacyResources;
+
+    public Builder setManifestFile(ArtifactLocation artifactLocation) {
+      this.manifest = artifactLocation;
+      return this;
+    }
+    public Builder addResource(ArtifactLocation artifactLocation) {
+      this.resources.add(artifactLocation);
+      return this;
+    }
+    public Builder setIdlJar(LibraryArtifact idlJar) {
+      this.idlJar = idlJar;
+      return this;
+    }
+    public Builder setHasIdlSources(boolean hasIdlSources) {
+      this.hasIdlSources = hasIdlSources;
+      return this;
+    }
+    public Builder setResourceJar(LibraryArtifact.Builder resourceJar) {
+      this.resourceJar = resourceJar.build();
+      return this;
+    }
+    public Builder setResourceJavaPackage(@Nullable String resourceJavaPackage) {
+      this.resourceJavaPackage = resourceJavaPackage;
+      return this;
+    }
+    public Builder setGenerateResourceClass(boolean generateResourceClass) {
+      this.generateResourceClass = generateResourceClass;
+      return this;
+    }
+    public Builder setLegacyResources(@Nullable Label legacyResources) {
+      this.legacyResources = legacyResources;
+      return this;
+    }
+    public AndroidRuleIdeInfo build() {
+      if (!resources.isEmpty() || manifest != null) {
+        if (!generateResourceClass) {
+          throw new IllegalStateException("Must set generateResourceClass if manifest or resources set");
+        }
+      }
+
+      return new AndroidRuleIdeInfo(
+        resources,
+        resourceJavaPackage,
+        generateResourceClass,
+        manifest,
+        idlJar,
+        resourceJar,
+        hasIdlSources,
+        legacyResources
+      );
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java
new file mode 100644
index 0000000..f40e633
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/ArtifactLocation.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.base.Objects;
+
+import java.io.File;
+import java.io.Serializable;
+import java.nio.file.Paths;
+
+/**
+ * Represents a blaze-produced artifact.
+ */
+public final class ArtifactLocation implements Serializable {
+  private static final long serialVersionUID = 2L;
+
+  public final String rootPath;
+  public final String rootExecutionPathFragment;
+  public final String relativePath;
+  public final boolean isSource;
+
+  private ArtifactLocation(String rootPath,
+                           String rootExecutionPathFragment,
+                           String relativePath,
+                           boolean isSource) {
+    this.rootPath = rootPath;
+    this.rootExecutionPathFragment = rootExecutionPathFragment;
+    this.relativePath = relativePath;
+    this.isSource = isSource;
+  }
+
+  /**
+   * Returns the root path of the artifact, eg. blaze-out
+   */
+  public String getRootPath() {
+    return rootPath;
+  }
+
+  /**
+   * Gets the path relative to the root path.
+   */
+  public String getRelativePath() {
+    return relativePath;
+  }
+
+  public boolean isSource() {
+    return isSource;
+  }
+
+  public boolean isGenerated() {
+    return !isSource;
+  }
+
+  public File getFile() {
+    return new File(getRootPath(), getRelativePath());
+  }
+
+  /**
+   * Returns rootExecutionPathFragment + relativePath.
+   * For source artifacts, this is simply relativePath
+   */
+  public String getExecutionRootRelativePath() {
+    return Paths.get(rootExecutionPathFragment, relativePath).toString();
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    String rootPath;
+    String relativePath;
+    String rootExecutionPathFragment = "";
+    boolean isSource;
+
+    public Builder setRootPath(String rootPath) {
+      this.rootPath = rootPath;
+      return this;
+    }
+
+    public Builder setRelativePath(String relativePath) {
+      this.relativePath = relativePath;
+      return this;
+    }
+
+    public Builder setRootExecutionPathFragment(String rootExecutionPathFragment) {
+      this.rootExecutionPathFragment = rootExecutionPathFragment;
+      return this;
+    }
+
+    public Builder setIsSource(boolean isSource) {
+      this.isSource = isSource;
+      return this;
+    }
+
+    public ArtifactLocation build() {
+      return new ArtifactLocation(rootPath, rootExecutionPathFragment, relativePath, isSource);
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ArtifactLocation that = (ArtifactLocation)o;
+    return Objects.equal(rootPath, that.rootPath)
+           && Objects.equal(rootExecutionPathFragment, that.rootExecutionPathFragment)
+           && Objects.equal(relativePath, that.relativePath)
+           && Objects.equal(isSource, that.isSource);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(rootPath, rootExecutionPathFragment, relativePath, isSource);
+  }
+
+  @Override
+  public String toString() {
+    return getFile().toString();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/CRuleIdeInfo.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/CRuleIdeInfo.java
new file mode 100644
index 0000000..641bf35
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/CRuleIdeInfo.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+
+import java.io.Serializable;
+
+/**
+ * Sister class to {@link JavaRuleIdeInfo}
+ */
+public class CRuleIdeInfo implements Serializable {
+  private static final long serialVersionUID = 6L;
+
+  public final ImmutableList<ArtifactLocation> sources;
+
+  // From the cpp compilation context provider. These should all be for the entire transitive closure.
+  public final ImmutableList<ExecutionRootPath> transitiveIncludeDirectories;
+  public final ImmutableList<ExecutionRootPath> transitiveQuoteIncludeDirectories;
+  public final ImmutableList<String> transitiveDefines;
+  public final ImmutableList<ExecutionRootPath> transitiveSystemIncludeDirectories;
+
+  public CRuleIdeInfo(
+    ImmutableList<ArtifactLocation> sources,
+    ImmutableList<ExecutionRootPath> transitiveIncludeDirectories,
+    ImmutableList<ExecutionRootPath> transitiveQuoteIncludeDirectories,
+    ImmutableList<String> transitiveDefines,
+    ImmutableList<ExecutionRootPath> transitiveSystemIncludeDirectories
+  ) {
+    this.sources = sources;
+    this.transitiveIncludeDirectories = transitiveIncludeDirectories;
+    this.transitiveQuoteIncludeDirectories = transitiveQuoteIncludeDirectories;
+    this.transitiveDefines = transitiveDefines;
+    this.transitiveSystemIncludeDirectories = transitiveSystemIncludeDirectories;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private final ImmutableList.Builder<ArtifactLocation> sources = ImmutableList.builder();
+
+    private final ImmutableList.Builder<ExecutionRootPath> transitiveIncludeDirectories = ImmutableList.builder();
+    private final ImmutableList.Builder<ExecutionRootPath> transitiveQuoteIncludeDirectories = ImmutableList.builder();
+    private final ImmutableList.Builder<String> transitiveDefines = ImmutableList.builder();
+    private final ImmutableList.Builder<ExecutionRootPath> transitiveSystemIncludeDirectories = ImmutableList.builder();
+
+    public Builder addSources(Iterable<ArtifactLocation> sources) {
+      this.sources.addAll(sources);
+      return this;
+    }
+
+    public Builder addTransitiveIncludeDirectories(Iterable<ExecutionRootPath> transitiveIncludeDirectories) {
+      this.transitiveIncludeDirectories.addAll(transitiveIncludeDirectories);
+      return this;
+    }
+
+    public Builder addTransitiveQuoteIncludeDirectories(Iterable<ExecutionRootPath> transitiveQuoteIncludeDirectories) {
+      this.transitiveQuoteIncludeDirectories.addAll(transitiveQuoteIncludeDirectories);
+      return this;
+    }
+
+    public Builder addTransitiveDefines(Iterable<String> transitiveDefines) {
+      this.transitiveDefines.addAll(transitiveDefines);
+      return this;
+    }
+
+    public Builder addTransitiveSystemIncludeDirectories(Iterable<ExecutionRootPath> transitiveSystemIncludeDirectories) {
+      this.transitiveSystemIncludeDirectories.addAll(transitiveSystemIncludeDirectories);
+      return this;
+    }
+
+    public CRuleIdeInfo build() {
+      return new CRuleIdeInfo(
+        sources.build(),
+        transitiveIncludeDirectories.build(),
+        transitiveQuoteIncludeDirectories.build(),
+        transitiveDefines.build(),
+        transitiveSystemIncludeDirectories.build()
+      );
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "CRuleIdeInfo{" + "\n" +
+           "  sources=" + sources + "\n" +
+           "  transitiveIncludeDirectories=" + transitiveIncludeDirectories + "\n" +
+           "  transitiveQuoteIncludeDirectories=" + transitiveQuoteIncludeDirectories + "\n" +
+           "  transitiveDefines=" + transitiveDefines + "\n" +
+           "  transitiveSystemIncludeDirectories=" + transitiveSystemIncludeDirectories + "\n" +
+           '}';
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/CToolchainIdeInfo.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/CToolchainIdeInfo.java
new file mode 100644
index 0000000..1f6b0f0
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/CToolchainIdeInfo.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+
+import java.io.Serializable;
+
+/**
+ * Sister class to {@link JavaRuleIdeInfo}
+ */
+public class CToolchainIdeInfo implements Serializable {
+  private static final long serialVersionUID = 3L;
+
+  public final ImmutableList<String> baseCompilerOptions;
+  public final ImmutableList<String> cCompilerOptions;
+  public final ImmutableList<String> cppCompilerOptions;
+  public final ImmutableList<String> linkOptions;
+  public final ImmutableList<ExecutionRootPath> builtInIncludeDirectories;
+  public final ExecutionRootPath cppExecutable;
+  public final ExecutionRootPath preprocessorExecutable;
+  public final String targetName;
+
+  public final ImmutableList<String> unfilteredCompilerOptions;
+  public final ImmutableList<ExecutionRootPath> unfilteredToolchainSystemIncludes;
+
+  public CToolchainIdeInfo(
+    ImmutableList<String> baseCompilerOptions,
+    ImmutableList<String> cCompilerOptions,
+    ImmutableList<String> cppCompilerOptions,
+    ImmutableList<String> linkOptions,
+    ImmutableList<ExecutionRootPath> builtInIncludeDirectories,
+    ExecutionRootPath cppExecutable,
+    ExecutionRootPath preprocessorExecutable,
+    String targetName,
+    ImmutableList<String> unfilteredCompilerOptions,
+    ImmutableList<ExecutionRootPath> unfilteredToolchainSystemIncludes
+  ) {
+    this.baseCompilerOptions = baseCompilerOptions;
+    this.cCompilerOptions = cCompilerOptions;
+    this.cppCompilerOptions = cppCompilerOptions;
+    this.linkOptions = linkOptions;
+    this.builtInIncludeDirectories = builtInIncludeDirectories;
+    this.cppExecutable = cppExecutable;
+    this.preprocessorExecutable = preprocessorExecutable;
+    this.targetName = targetName;
+    this.unfilteredCompilerOptions = unfilteredCompilerOptions;
+    this.unfilteredToolchainSystemIncludes = unfilteredToolchainSystemIncludes;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private final ImmutableList.Builder<String> baseCompilerOptions = ImmutableList.builder();
+    private final ImmutableList.Builder<String> cCompilerOptions = ImmutableList.builder();
+    private final ImmutableList.Builder<String> cppCompilerOptions = ImmutableList.builder();
+    private final ImmutableList.Builder<String> linkOptions = ImmutableList.builder();
+
+    private final ImmutableList.Builder<ExecutionRootPath> builtInIncludeDirectories = ImmutableList.builder();
+
+    ExecutionRootPath cppExecutable;
+    ExecutionRootPath preprocessorExecutable;
+
+    String targetName = "";
+
+    private final ImmutableList.Builder<String> unfilteredCompilerOptions = ImmutableList.builder();
+    private final ImmutableList.Builder<ExecutionRootPath> unfilteredToolchainSystemIncludes = ImmutableList.builder();
+
+    public Builder addBaseCompilerOptions(Iterable<String> baseCompilerOptions) {
+      this.baseCompilerOptions.addAll(baseCompilerOptions);
+      return this;
+    }
+
+    public Builder addCCompilerOptions(Iterable<String> cCompilerOptions) {
+      this.cCompilerOptions.addAll(cCompilerOptions);
+      return this;
+    }
+
+    public Builder addCppCompilerOptions(Iterable<String> cppCompilerOptions) {
+      this.cppCompilerOptions.addAll(cppCompilerOptions);
+      return this;
+    }
+
+    public Builder addLinkOptions(Iterable<String> linkOptions) {
+      this.linkOptions.addAll(linkOptions);
+      return this;
+    }
+
+    public Builder addBuiltInIncludeDirectories(Iterable<ExecutionRootPath> builtInIncludeDirectories) {
+      this.builtInIncludeDirectories.addAll(builtInIncludeDirectories);
+      return this;
+    }
+
+    public Builder setCppExecutable(ExecutionRootPath cppExecutable) {
+      this.cppExecutable = cppExecutable;
+      return this;
+    }
+
+    public Builder setPreprocessorExecutable(ExecutionRootPath preprocessorExecutable) {
+      this.preprocessorExecutable = preprocessorExecutable;
+      return this;
+    }
+
+    public Builder setTargetName(String targetName) {
+      this.targetName = targetName;
+      return this;
+    }
+
+    public Builder addUnfilteredCompilerOptions(Iterable<String> unfilteredCompilerOptions) {
+      this.unfilteredCompilerOptions.addAll(unfilteredCompilerOptions);
+      return this;
+    }
+
+    public Builder addUnfilteredToolchainSystemIncludes(Iterable<ExecutionRootPath> unfilteredToolchainSystemIncludes) {
+      this.unfilteredToolchainSystemIncludes.addAll(unfilteredToolchainSystemIncludes);
+      return this;
+    }
+
+    public CToolchainIdeInfo build() {
+      return new CToolchainIdeInfo(
+        baseCompilerOptions.build(),
+        cCompilerOptions.build(),
+        cppCompilerOptions.build(),
+        linkOptions.build(),
+        builtInIncludeDirectories.build(),
+        cppExecutable,
+        preprocessorExecutable,
+        targetName,
+        unfilteredCompilerOptions.build(),
+        unfilteredToolchainSystemIncludes.build()
+      );
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "CToolchainIdeInfo{" + "\n" +
+           "  baseCompilerOptions=" + baseCompilerOptions + "\n" +
+           "  cCompilerOptions=" + cCompilerOptions + "\n" +
+           "  cppCompilerOptions=" + cppCompilerOptions + "\n" +
+           "  linkOptions=" + linkOptions + "\n" +
+           "  builtInIncludeDirectories=" + builtInIncludeDirectories + "\n" +
+           "  cppExecutable='" + cppExecutable + '\'' + "\n" +
+           "  preprocessorExecutable='" + preprocessorExecutable + '\'' + "\n" +
+           "  targetName='" + targetName + '\'' + "\n" +
+           "  unfilteredCompilerOptions=" + unfilteredCompilerOptions + "\n" +
+           "  unfilteredToolchainSystemIncludes=" + unfilteredToolchainSystemIncludes + "\n" +
+           '}';
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    CToolchainIdeInfo that = (CToolchainIdeInfo)o;
+    return
+      Objects.equal(baseCompilerOptions, that.baseCompilerOptions) &&
+      Objects.equal(cCompilerOptions, that.cCompilerOptions) &&
+      Objects.equal(cppCompilerOptions, that.cppCompilerOptions) &&
+      Objects.equal(linkOptions, that.linkOptions) &&
+      Objects.equal(builtInIncludeDirectories, that.builtInIncludeDirectories) &&
+      Objects.equal(cppExecutable, that.cppExecutable) &&
+      Objects.equal(preprocessorExecutable, that.preprocessorExecutable) &&
+      Objects.equal(targetName, that.targetName) &&
+      Objects.equal(unfilteredCompilerOptions, that.unfilteredCompilerOptions) &&
+      Objects.equal(unfilteredToolchainSystemIncludes, that.unfilteredToolchainSystemIncludes)
+      ;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(
+      baseCompilerOptions,
+      cCompilerOptions,
+      cppCompilerOptions,
+      linkOptions,
+      builtInIncludeDirectories,
+      cppExecutable,
+      preprocessorExecutable,
+      targetName,
+      unfilteredCompilerOptions,
+      unfilteredToolchainSystemIncludes
+    );
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/JavaRuleIdeInfo.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/JavaRuleIdeInfo.java
new file mode 100644
index 0000000..31eb533
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/JavaRuleIdeInfo.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.ImmutableList;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Ide info specific to java rules.
+ */
+public final class JavaRuleIdeInfo implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  /**
+   * The main jar(s) produced by this java rule.
+   *
+   * <p>Usually this will be a single jar, but java_imports support importing multiple jars.
+   */
+  public final Collection<LibraryArtifact> jars;
+
+  /**
+   * A jar containing annotation processing.
+   */
+  public final Collection<LibraryArtifact> generatedJars;
+
+  /**
+   * File containing a map from .java files to their corresponding package.
+   */
+  @Nullable public final ArtifactLocation packageManifest;
+
+  /**
+   * File containing dependencies.
+   */
+  @Nullable public final ArtifactLocation jdepsFile;
+
+  public JavaRuleIdeInfo(Collection<LibraryArtifact> jars,
+                         Collection<LibraryArtifact> generatedJars,
+                         @Nullable ArtifactLocation packageManifest,
+                         @Nullable ArtifactLocation jdepsFile) {
+    this.jars = jars;
+    this.generatedJars = generatedJars;
+    this.packageManifest = packageManifest;
+    this.jdepsFile = jdepsFile;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    ImmutableList.Builder<LibraryArtifact> jars = ImmutableList.builder();
+    ImmutableList.Builder<LibraryArtifact> generatedJars = ImmutableList.builder();
+
+    public Builder addJar(LibraryArtifact.Builder jar) {
+      jars.add(jar.build());
+      return this;
+    }
+
+    public Builder addGeneratedJar(LibraryArtifact.Builder jar) {
+      generatedJars.add(jar.build());
+      return this;
+    }
+
+    public JavaRuleIdeInfo build() {
+      return new JavaRuleIdeInfo(jars.build(), generatedJars.build(), null, null);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/JavaToolchainIdeInfo.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/JavaToolchainIdeInfo.java
new file mode 100644
index 0000000..1fcbf41
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/JavaToolchainIdeInfo.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import java.io.Serializable;
+
+/**
+ * Represents the java_toolchain class
+ */
+public class JavaToolchainIdeInfo implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final String sourceVersion;
+  public final String targetVersion;
+
+  public JavaToolchainIdeInfo(String sourceVersion, String targetVersion) {
+    this.sourceVersion = sourceVersion;
+    this.targetVersion = targetVersion;
+  }
+
+  @Override
+  public String toString() {
+    return "JavaToolchainIdeInfo{" + "\n" +
+           "  sourceVersion=" + sourceVersion + "\n" +
+           "  targetVersion=" + targetVersion + "\n" +
+           '}';
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    String sourceVersion;
+    String targetVersion;
+
+    public Builder setSourceVersion(String sourceVersion) {
+      this.sourceVersion = sourceVersion;
+      return this;
+    }
+
+    public Builder setTargetVersion(String targetVersion) {
+      this.targetVersion = targetVersion;
+      return this;
+    }
+
+    public JavaToolchainIdeInfo build() {
+      return new JavaToolchainIdeInfo(sourceVersion, targetVersion);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java
new file mode 100644
index 0000000..44f581c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/LibraryArtifact.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.base.Objects;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.Serializable;
+
+/**
+ * Represents a jar artifact.
+ */
+public class LibraryArtifact implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final ArtifactLocation jar;
+  @Nullable public final ArtifactLocation runtimeJar;
+  @Nullable public final ArtifactLocation sourceJar;
+
+  public LibraryArtifact(ArtifactLocation jar, @Nullable ArtifactLocation runtimeJar, @Nullable ArtifactLocation sourceJar) {
+    this.jar = jar;
+    this.runtimeJar = runtimeJar;
+    this.sourceJar = sourceJar;
+  }
+
+  @Override
+  public String toString() {
+    return String.format("jar=%s, ijar=%s, srcjar=%s", runtimeJar, jar, sourceJar);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    LibraryArtifact that = (LibraryArtifact)o;
+    return
+      Objects.equal(jar, that.jar) &&
+      Objects.equal(runtimeJar, that.runtimeJar) &&
+      Objects.equal(sourceJar, that.sourceJar);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(jar, runtimeJar, sourceJar);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private ArtifactLocation jar;
+    private ArtifactLocation runtimeJar;
+    private ArtifactLocation sourceJar;
+
+    public Builder setJar(ArtifactLocation artifactLocation) {
+      this.jar = artifactLocation;
+      return this;
+    }
+    public Builder setRuntimeJar(@Nullable ArtifactLocation artifactLocation) {
+      this.runtimeJar = artifactLocation;
+      return this;
+    }
+    public Builder setSourceJar(@Nullable ArtifactLocation artifactLocation) {
+      this.sourceJar = artifactLocation;
+      return this;
+    }
+    public LibraryArtifact build() {
+      return new LibraryArtifact(jar, runtimeJar, sourceJar);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/ProtoLibraryLegacyInfo.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/ProtoLibraryLegacyInfo.java
new file mode 100644
index 0000000..18f6b69
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/ProtoLibraryLegacyInfo.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.ImmutableList;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Proto library info for legacy proto libraries.
+ *
+ * Replicates blaze semantics.
+ */
+public class ProtoLibraryLegacyInfo implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public enum ApiFlavor {
+    VERSION_1,
+    MUTABLE,
+    IMMUTABLE,
+    BOTH,
+    NONE,
+  }
+
+  public final ApiFlavor apiFlavor;
+
+  public final Collection<LibraryArtifact> jarsV1;
+  public final Collection<LibraryArtifact> jarsMutable;
+  public final Collection<LibraryArtifact> jarsImmutable;
+
+  public ProtoLibraryLegacyInfo(ApiFlavor apiFlavor,
+                                Collection<LibraryArtifact> jarsV1,
+                                Collection<LibraryArtifact> jarsMutable,
+                                Collection<LibraryArtifact> jarsImmutable) {
+    this.apiFlavor = apiFlavor;
+    this.jarsV1 = jarsV1;
+    this.jarsMutable = jarsMutable;
+    this.jarsImmutable = jarsImmutable;
+  }
+
+  public static Builder builder(ApiFlavor apiFlavor) {
+    return new Builder(apiFlavor);
+  }
+
+  public static class Builder {
+    private final ApiFlavor apiFlavor;
+    private ImmutableList.Builder<LibraryArtifact> jarsV1 = ImmutableList.builder();
+    private ImmutableList.Builder<LibraryArtifact> jarsMutable = ImmutableList.builder();
+    private ImmutableList.Builder<LibraryArtifact> jarsImmutable = ImmutableList.builder();
+
+    Builder(ApiFlavor apiFlavor) {
+      this.apiFlavor = apiFlavor;
+    }
+
+    public Builder addJarV1(LibraryArtifact.Builder library) {
+      jarsV1.add(library.build());
+      return this;
+    }
+
+    public Builder addJarMutable(LibraryArtifact.Builder library) {
+      jarsMutable.add(library.build());
+      return this;
+    }
+
+    public Builder addJarImmutable(LibraryArtifact.Builder library) {
+      jarsImmutable.add(library.build());
+      return this;
+    }
+
+    public ProtoLibraryLegacyInfo build() {
+      return new ProtoLibraryLegacyInfo(apiFlavor, jarsV1.build(), jarsMutable.build(), jarsImmutable.build());
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/RuleIdeInfo.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/RuleIdeInfo.java
new file mode 100644
index 0000000..896e60d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/RuleIdeInfo.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+
+import javax.annotation.Nullable;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Simple implementation of RuleIdeInfo.
+ */
+public final class RuleIdeInfo implements Serializable {
+  private static final long serialVersionUID = 10L;
+
+  public final Label label;
+  public final Kind kind;
+  @Nullable public final ArtifactLocation buildFile;
+  public final Collection<Label> dependencies;
+  public final Collection<Label> runtimeDeps;
+  public final Collection<String> tags;
+  public final Collection<ArtifactLocation> sources;
+  @Nullable public final CRuleIdeInfo cRuleIdeInfo;
+  @Nullable public final CToolchainIdeInfo cToolchainIdeInfo;
+  @Nullable public final JavaRuleIdeInfo javaRuleIdeInfo;
+  @Nullable public final AndroidRuleIdeInfo androidRuleIdeInfo;
+  @Nullable public final TestIdeInfo testIdeInfo;
+  @Nullable public final ProtoLibraryLegacyInfo protoLibraryLegacyInfo;
+  @Nullable public final JavaToolchainIdeInfo javaToolchainIdeInfo;
+
+  public RuleIdeInfo(Label label,
+                     Kind kind,
+                     @Nullable ArtifactLocation buildFile,
+                     Collection<Label> dependencies,
+                     Collection<Label> runtimeDeps,
+                     Collection<String> tags,
+                     Collection<ArtifactLocation> sources,
+                     @Nullable CRuleIdeInfo cRuleIdeInfo,
+                     @Nullable CToolchainIdeInfo cToolchainIdeInfo,
+                     @Nullable JavaRuleIdeInfo javaRuleIdeInfo,
+                     @Nullable AndroidRuleIdeInfo androidRuleIdeInfo,
+                     @Nullable TestIdeInfo testIdeInfo,
+                     @Nullable ProtoLibraryLegacyInfo protoLibraryLegacyInfo,
+                     @Nullable JavaToolchainIdeInfo javaToolchainIdeInfo) {
+    this.label = label;
+    this.kind = kind;
+    this.buildFile = buildFile;
+    this.dependencies = dependencies;
+    this.runtimeDeps = runtimeDeps;
+    this.tags = tags;
+    this.sources = sources;
+    this.cRuleIdeInfo = cRuleIdeInfo;
+    this.cToolchainIdeInfo = cToolchainIdeInfo;
+    this.javaRuleIdeInfo = javaRuleIdeInfo;
+    this.androidRuleIdeInfo = androidRuleIdeInfo;
+    this.testIdeInfo = testIdeInfo;
+    this.protoLibraryLegacyInfo = protoLibraryLegacyInfo;
+    this.javaToolchainIdeInfo = javaToolchainIdeInfo;
+  }
+
+  @Override
+  public String toString() {
+    return label.toString();
+  }
+
+  /**
+   * Returns whether this rule is one of the kinds.
+   */
+  public boolean kindIsOneOf(Kind... kinds) {
+    return kindIsOneOf(Arrays.asList(kinds));
+  }
+
+  /**
+   * Returns whether this rule is one of the kinds.
+   */
+  public boolean kindIsOneOf(List<Kind> kinds) {
+    if (kind != null) {
+      return kind.isOneOf(kinds);
+    }
+    return false;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private Label label;
+    private Kind kind;
+    private ArtifactLocation buildFile;
+    private final List<Label> dependencies = Lists.newArrayList();
+    private final List<Label> runtimeDeps = Lists.newArrayList();
+    private final List<String> tags = Lists.newArrayList();
+    private final List<ArtifactLocation> sources = Lists.newArrayList();
+    private final List<LibraryArtifact> libraries = Lists.newArrayList();
+    private CRuleIdeInfo cRuleIdeInfo;
+    private CToolchainIdeInfo cToolchainIdeInfo;
+    private JavaRuleIdeInfo javaRuleIdeInfo;
+    private AndroidRuleIdeInfo androidRuleIdeInfo;
+    private TestIdeInfo testIdeInfo;
+    private ProtoLibraryLegacyInfo protoLibraryLegacyInfo;
+    private JavaToolchainIdeInfo javaToolchainIdeInfo;
+
+    public Builder setLabel(String label) {
+      return setLabel(new Label(label));
+    }
+    public Builder setLabel(Label label) {
+      this.label = label;
+      return this;
+    }
+    public Builder setBuildFile(ArtifactLocation buildFile) {
+      this.buildFile = buildFile;
+      return this;
+    }
+    public Builder setKind(String kind) {
+      return setKind(Kind.fromString(kind));
+    }
+    public Builder setKind(Kind kind) {
+      this.kind = kind;
+      return this;
+    }
+    public Builder addSource(ArtifactLocation source) {
+      this.sources.add(source);
+      return this;
+    }
+    public Builder addSource(ArtifactLocation.Builder source) {
+      return addSource(source.build());
+    }
+    public Builder setJavaInfo(JavaRuleIdeInfo.Builder builder) {
+      javaRuleIdeInfo = builder.build();
+      return this;
+    }
+    public Builder setCInfo(CRuleIdeInfo cInfo) {
+      this.cRuleIdeInfo = cInfo;
+      return this;
+    }
+    public Builder setCInfo(CRuleIdeInfo.Builder cInfo) {
+      return setCInfo(cInfo.build());
+    }
+    public Builder setCToolchainInfo(CToolchainIdeInfo info) {
+      this.cToolchainIdeInfo = info;
+      return this;
+    }
+    public Builder setCToolchainInfo(CToolchainIdeInfo.Builder info) {
+      return setCToolchainInfo(info.build());
+    }
+    public Builder setAndroidInfo(AndroidRuleIdeInfo androidInfo) {
+      this.androidRuleIdeInfo = androidInfo;
+      return this;
+    }
+    public Builder setAndroidInfo(AndroidRuleIdeInfo.Builder androidInfo) {
+      return setAndroidInfo(androidInfo.build());
+    }
+    public Builder setTestInfo(TestIdeInfo.Builder testInfo) {
+      this.testIdeInfo = testInfo.build();
+      return this;
+    }
+    public Builder setProtoLibraryLegacyInfo(ProtoLibraryLegacyInfo.Builder protoLibraryLegacyInfo) {
+      this.protoLibraryLegacyInfo = protoLibraryLegacyInfo.build();
+      return this;
+    }
+    public Builder setJavaToolchainIdeInfo(JavaToolchainIdeInfo.Builder javaToolchainIdeInfo) {
+      this.javaToolchainIdeInfo = javaToolchainIdeInfo.build();
+      return this;
+    }
+    public Builder addTag(String s) {
+      this.tags.add(s);
+      return this;
+    }
+    public Builder addDependency(String s) {
+      return addDependency(new Label(s));
+    }
+    public Builder addDependency(Label label) {
+      this.dependencies.add(label);
+      return this;
+    }
+    public Builder addRuntimeDep(String s) {
+      return addRuntimeDep(new Label(s));
+    }
+    public Builder addRuntimeDep(Label label) {
+      this.runtimeDeps.add(label);
+      return this;
+    }
+    public RuleIdeInfo build() {
+      return new RuleIdeInfo(
+        label,
+        kind,
+        buildFile,
+        dependencies,
+        runtimeDeps,
+        tags,
+        sources,
+        cRuleIdeInfo,
+        cToolchainIdeInfo,
+        javaRuleIdeInfo,
+        androidRuleIdeInfo,
+        testIdeInfo,
+        protoLibraryLegacyInfo,
+        javaToolchainIdeInfo
+      );
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/Tags.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/Tags.java
new file mode 100644
index 0000000..82af46f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/Tags.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+/**
+ * Tag constants used by our rules.
+ */
+public class Tags {
+  /**
+   * Forces import of the target output.
+   */
+  public static final String RULE_TAG_IMPORT_TARGET_OUTPUT = "intellij-import-target-output";
+  public static final String RULE_TAG_IMPORT_AS_LIBRARY_LEGACY = "aswb-import-as-library";
+
+  /**
+   * Signals to the import process that the output of this rule will be provided by the IntelliJ SDK.
+   */
+  public static final String RULE_TAG_PROVIDED_BY_SDK = "intellij-provided-by-sdk";
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ideinfo/TestIdeInfo.java b/blaze-base/src/com/google/idea/blaze/base/ideinfo/TestIdeInfo.java
new file mode 100644
index 0000000..534f09c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ideinfo/TestIdeInfo.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import javax.annotation.Nullable;
+import java.io.Serializable;
+
+/**
+ * Test info.
+ */
+public class TestIdeInfo implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public enum TestSize {
+    SMALL,
+    MEDIUM,
+    LARGE,
+    ENORMOUS
+  }
+
+  // Rules are "medium" test size by default
+  public static final TestSize DEFAULT_RULE_TEST_SIZE = TestSize.MEDIUM;
+
+  // Non-annotated methods and classes are "small" by default
+  public static final TestSize DEFAULT_NON_ANNOTATED_TEST_SIZE = TestSize.SMALL;
+
+  public final TestSize testSize;
+
+  public TestIdeInfo(TestSize testSize) {
+    this.testSize = testSize;
+  }
+
+  @Nullable
+  static public TestSize getTestSize(RuleIdeInfo rule) {
+    TestIdeInfo testIdeInfo = rule.testIdeInfo;
+    if (testIdeInfo == null) {
+      return null;
+    }
+    return testIdeInfo.testSize;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private TestSize testSize = DEFAULT_RULE_TEST_SIZE;
+
+    public Builder setTestSize(TestSize testSize) {
+      this.testSize = testSize;
+      return this;
+    }
+
+    public TestIdeInfo build() {
+      return new TestIdeInfo(testSize);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/io/FileAttributeProvider.java b/blaze-base/src/com/google/idea/blaze/base/io/FileAttributeProvider.java
new file mode 100644
index 0000000..829d262
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/io/FileAttributeProvider.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.base.io;
+
+import com.intellij.openapi.components.ServiceManager;
+
+import java.io.File;
+
+/**
+ * Simple file system checks (existence, isDirectory)
+ */
+public class FileAttributeProvider {
+
+  public static FileAttributeProvider getInstance() {
+    return ServiceManager.getService(FileAttributeProvider.class);
+  }
+
+  public boolean exists(File file) {
+    return file.exists();
+  }
+
+  public boolean isDirectory(File file) {
+    return file.isDirectory();
+  }
+
+  public boolean isFile(File file) {
+    return file.isFile();
+  }
+
+  public long getFileModifiedTime(File file) {
+    return file.lastModified();
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/io/InputStreamProvider.java b/blaze-base/src/com/google/idea/blaze/base/io/InputStreamProvider.java
new file mode 100644
index 0000000..542992f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/io/InputStreamProvider.java
@@ -0,0 +1,34 @@
+/*
+ * 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.io;
+
+import com.intellij.openapi.components.ServiceManager;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Provides input streams for files.
+ */
+public interface InputStreamProvider {
+
+  static InputStreamProvider getInstance() {
+    return ServiceManager.getService(InputStreamProvider.class);
+  }
+
+  InputStream getFile(File file) throws IOException;
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/io/InputStreamProviderImpl.java b/blaze-base/src/com/google/idea/blaze/base/io/InputStreamProviderImpl.java
new file mode 100644
index 0000000..a4f0d64
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/io/InputStreamProviderImpl.java
@@ -0,0 +1,34 @@
+/*
+ * 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.io;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+
+/**
+ * Default implementation of InputStreamProvider.
+ */
+final class InputStreamProviderImpl implements InputStreamProvider {
+
+  @Override
+  public InputStream getFile(@NotNull File file) throws FileNotFoundException {
+    return new FileInputStream(file);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/io/VfsWorkspaceScanner.java b/blaze-base/src/com/google/idea/blaze/base/io/VfsWorkspaceScanner.java
new file mode 100644
index 0000000..5ca8daf
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/io/VfsWorkspaceScanner.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.io;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+
+/**
+ * Checks the workspace using the VFS.
+ */
+class VfsWorkspaceScanner implements WorkspaceScanner {
+  private final LocalFileSystem localFileSystem;
+
+  public VfsWorkspaceScanner() {
+    this.localFileSystem = LocalFileSystem.getInstance();
+  }
+
+  @Override
+  public boolean exists(WorkspaceRoot workspaceRoot, WorkspacePath workspacePath) {
+    VirtualFile virtualFile = localFileSystem.refreshAndFindFileByPath(
+      workspaceRoot.fileForPath(workspacePath).getPath()
+    );
+    return virtualFile != null && virtualFile.exists();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/io/WorkspaceScanner.java b/blaze-base/src/com/google/idea/blaze/base/io/WorkspaceScanner.java
new file mode 100644
index 0000000..bf4b4e0
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/io/WorkspaceScanner.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.io;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.components.ServiceManager;
+
+/**
+ * Used to scan the file system
+ */
+public interface WorkspaceScanner {
+  static WorkspaceScanner getInstance() {
+    return ServiceManager.getService(WorkspaceScanner.class);
+  }
+
+  boolean exists(WorkspaceRoot workspaceRoot, WorkspacePath workspacePath);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java b/blaze-base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
new file mode 100644
index 0000000..0890bf5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/issueparser/BlazeIssueParser.java
@@ -0,0 +1,331 @@
+/*
+ * 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.issueparser;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Lists;
+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 org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Parses blaze output for compile errors.
+ */
+public class BlazeIssueParser {
+
+  private static class ParseResult {
+
+    public static final ParseResult NEEDS_MORE_INPUT = new ParseResult(true, null);
+
+    public static final ParseResult NO_RESULT = new ParseResult(false, null);
+
+    private boolean needsMoreInput;
+    @Nullable private IssueOutput output;
+
+    private ParseResult(boolean needsMoreInput, IssueOutput output) {
+      this.needsMoreInput = needsMoreInput;
+      this.output = output;
+    }
+
+    public static ParseResult needsMoreInput() {
+      return NEEDS_MORE_INPUT;
+    }
+
+    public static ParseResult output(IssueOutput output) {
+      return new ParseResult(false, output);
+    }
+
+    public static ParseResult noResult() {
+      return NO_RESULT;
+    }
+  }
+
+  interface Parser {
+    @NotNull
+    ParseResult parse(@NotNull String currentLine, @NotNull List<String> previousLines);
+  }
+
+  static abstract class SingleLineParser implements Parser {
+    @NotNull
+    Pattern pattern;
+
+    SingleLineParser(@NotNull String regex) {
+      pattern = Pattern.compile(regex);
+    }
+
+    @Override
+    public ParseResult parse(@NotNull String currentLine, @NotNull List<String> multilineMatchResult) {
+      checkState(multilineMatchResult.isEmpty(), "SingleLineParser recieved multiple lines of input");
+      return parse(currentLine);
+    }
+
+    ParseResult parse(@NotNull String line) {
+      Matcher matcher = pattern.matcher(line);
+      if (matcher.find()) {
+        return ParseResult.output(createIssue(matcher));
+      }
+      return ParseResult.noResult();
+    }
+
+    @Nullable
+    protected abstract IssueOutput createIssue(@NotNull Matcher matcher);
+  }
+
+  static class CompileParser extends SingleLineParser {
+    @NotNull
+    private final WorkspaceRoot workspaceRoot;
+
+    public CompileParser(@NotNull 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;
+      }
+
+      IssueOutput.Category type = matcher.group(4).equals("error")
+                                  ? IssueOutput.Category.ERROR
+                                  : IssueOutput.Category.WARNING;
+      return IssueOutput.issue(type, matcher.group(5))
+        .inFile(file)
+        .onLine(Integer.parseInt(matcher.group(2)))
+        .build();
+    }
+  }
+
+  static class TracebackParser implements Parser {
+    private static final Pattern PATTERN = Pattern.compile("(ERROR): (.*?):([0-9]+):([0-9]+): (Traceback \\(most recent call last\\):)");
+
+    @NotNull
+    @Override
+    public ParseResult parse(@NotNull String currentLine, @NotNull List<String> previousLines) {
+      if (previousLines.isEmpty()) {
+        if (PATTERN.matcher(currentLine).find()) {
+          return ParseResult.needsMoreInput();
+        }
+        else {
+          return ParseResult.noResult();
+        }
+      }
+
+      if (currentLine.startsWith("\t")) {
+        return ParseResult.needsMoreInput();
+      }
+      else {
+        Matcher matcher = PATTERN.matcher(previousLines.get(0));
+        checkState(matcher.find(), "Found a match in the first line previously, but now it isn't there.");
+        StringBuilder message = new StringBuilder(matcher.group(5));
+        for (int i = 1; i < previousLines.size(); ++i) {
+          message.append(System.lineSeparator())
+            .append(previousLines.get(i));
+        }
+        message.append(System.lineSeparator())
+          .append(currentLine);
+        return ParseResult.output(IssueOutput.error(message.toString())
+                                    .inFile(new File(matcher.group(2)))
+                                    .onLine(Integer.parseInt(matcher.group(3)))
+                                    .build());
+      }
+    }
+  }
+
+  static class BuildParser extends SingleLineParser {
+    BuildParser() {
+      super("(ERROR): (.*?):([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)))
+        .build();
+    }
+  }
+
+  static class LinelessBuildParser extends SingleLineParser {
+    LinelessBuildParser() {
+      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();
+    }
+  }
+
+  static class ProjectViewLabelParser extends SingleLineParser {
+
+    @Nullable private final ProjectViewSet projectViewSet;
+
+    ProjectViewLabelParser(
+      @Nullable ProjectViewSet projectViewSet) {
+      super("no such target '(.*)': target .*? not declared in package .*? defined by");
+      this.projectViewSet = projectViewSet;
+    }
+
+    @Override
+    protected IssueOutput createIssue(@NotNull Matcher matcher) {
+      File file = null;
+      if (projectViewSet != null) {
+        String targetString = matcher.group(1);
+        final TargetExpression targetExpression = TargetExpression.fromString(targetString);
+        file = projectViewFileWithSection(projectViewSet, TargetSection.KEY, new Predicate<ListSection<TargetExpression>>() {
+          @Override
+          public boolean apply(@NotNull ListSection<TargetExpression> targetSection) {
+            return targetSection.items().contains(targetExpression);
+          }
+        });
+      }
+
+      return IssueOutput.error(matcher.group(0))
+        .inFile(file)
+        .build();
+    }
+  }
+
+  static class InvalidTargetProjectViewPackageParser extends SingleLineParser {
+    @Nullable private final ProjectViewSet projectViewSet;
+
+    InvalidTargetProjectViewPackageParser(
+      @Nullable ProjectViewSet projectViewSet,
+      String regex) {
+      super(regex);
+      this.projectViewSet = projectViewSet;
+    }
+
+    @Override
+    protected IssueOutput createIssue(@NotNull Matcher matcher) {
+      File file = null;
+      if (projectViewSet != null) {
+        final String packageString = matcher.group(1);
+        file = projectViewFileWithSection(projectViewSet, TargetSection.KEY, targetSection -> {
+          for (TargetExpression targetExpression : targetSection.items()) {
+            if (targetExpression.toString().startsWith("//" + packageString + ":")) {
+              return true;
+            }
+          }
+          return false;
+        });
+      }
+
+      return IssueOutput.error(matcher.group(0))
+        .inFile(file)
+        .build();
+    }
+  }
+
+  @Nullable
+  private static <T, SectionType extends Section<T>> File projectViewFileWithSection(
+    @NotNull ProjectViewSet projectViewSet,
+    @NotNull SectionKey<T, SectionType> key,
+    @NotNull Predicate<SectionType> predicate) {
+    for (ProjectViewSet.ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
+      SectionType section = projectViewFile.projectView.getSectionOfType(key);
+      if (section != null && predicate.apply(section)) {
+        return projectViewFile.projectViewFile;
+      }
+    }
+    return null;
+  }
+
+  @NotNull private List<Parser> parsers = Lists.newArrayList();
+  /** 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<>();
+
+  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 '(.*)'"));
+  }
+
+
+  @Nullable
+  public IssueOutput parseIssue(String line) {
+
+    List<Parser> parsers = this.parsers;
+    if (multilineMatchingParser != null) {
+      parsers = Lists.newArrayList(multilineMatchingParser);
+    }
+
+    for (Parser parser : parsers) {
+      ParseResult issue = parser.parse(line, multilineMatchResult);
+      if (issue.needsMoreInput) {
+        multilineMatchingParser = parser;
+        multilineMatchResult.add(line);
+        return null;
+      }
+      else {
+        multilineMatchingParser = null;
+        multilineMatchResult = new ArrayList<>();
+      }
+
+      if (issue.output != null) {
+        return issue.output;
+      }
+    }
+
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java b/blaze-base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.java
new file mode 100644
index 0000000..3b34a42
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/issueparser/IssueOutputLineProcessor.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.issueparser;
+
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+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;
+
+/**
+ * Forwards output to PrintOutputs, colored by whether or not
+ * an issue is found per-line.
+ * <p/>
+ * Also creates IssueOutput if issues are found.
+ */
+public class IssueOutputLineProcessor
+  implements LineProcessingOutputStream.LineProcessor {
+
+  @NotNull
+  private final BlazeContext context;
+
+  @NotNull
+  private final BlazeIssueParser blazeIssueParser;
+
+  public IssueOutputLineProcessor(
+    @Nullable Project project,
+    @NotNull BlazeContext context,
+    @NotNull WorkspaceRoot workspaceRoot) {
+    this.context = context;
+    this.blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+  }
+
+  @Override
+  public boolean processLine(@NotNull String line) {
+    IssueOutput issue = blazeIssueParser.parseIssue(line);
+    if (issue != null) {
+      if (issue.getCategory() == IssueOutput.Category.ERROR) {
+        context.setHasError();
+      }
+      context.output(issue);
+    }
+
+    OutputType outputType = issue == null
+                            ? OutputType.NORMAL : OutputType.ERROR;
+
+    context.output(new PrintOutput(line, outputType));
+    return true;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierImpl.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierImpl.java
new file mode 100644
index 0000000..b8b7d36
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/actions/BuildFileModifierImpl.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.actions;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.buildmodifier.BuildFileModifier;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.Expression;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.BuildElementGenerator;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+
+/**
+ * Implementation of BuildFileModifier. Modifies the PSI tree directly.
+ */
+public class BuildFileModifierImpl implements BuildFileModifier {
+
+  private static final Logger LOG = Logger.getInstance(BuildFileModifierImpl.class);
+
+  @Override
+  public boolean addRule(Project project,
+                         BlazeContext context,
+                         Label newRule,
+                         Kind ruleKind) {
+    BuildFile buildFile = BuildReferenceManager.getInstance(project).resolveBlazePackage(newRule.blazePackage());
+    if (buildFile == null) {
+      LOG.error("No BUILD file found at location: " + newRule.blazePackage());
+      return false;
+    }
+    WriteCommandAction.runWriteCommandAction(project, () -> {
+      buildFile.add(createRule(project, ruleKind, newRule.ruleName().toString()));
+    });
+    return true;
+  }
+
+  private PsiElement createRule(Project project, Kind ruleKind, String ruleName) {
+    String text = Joiner.on("\n").join(
+      ruleKind.toString() + "(",
+      "    name = \"" + ruleName + "\"",
+      ")"
+    );
+    Expression expr = BuildElementGenerator.getInstance(project).createExpressionFromText(text);
+    assert(expr instanceof FuncallExpression);
+    return expr;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributor.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributor.java
new file mode 100644
index 0000000..f0eff50
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributor.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.completion;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.psi.util.PsiTreeUtil;
+import com.intellij.util.ProcessingContext;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * We can't rely solely keyword arg references, because as the user is typing a new keyword arg,
+ * the PsiElement will be a ReferenceExpression (with different completion results
+ * not relevant to keyword args).
+ */
+public class ArgumentCompletionContributor 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 ArgumentCompletionContributor() {
+    extend(
+      CompletionType.BASIC,
+      psiElement()
+        .withLanguage(BuildFileLanguage.INSTANCE)
+        .withElementType(BuildToken.fromKind(TokenKind.IDENTIFIER))
+        .withParents(ReferenceExpression.class, Argument.Positional.class)
+        .andNot(psiElement().afterLeaf("="))
+        .andNot(psiElement().afterLeaf(psiElement(BuildToken.fromKind(TokenKind.IDENTIFIER)))),
+      new CompletionProvider<CompletionParameters>() {
+        @Override
+        protected void addCompletions(CompletionParameters parameters, ProcessingContext context, CompletionResultSet result) {
+          Argument.Positional arg = PsiTreeUtil.getParentOfType(parameters.getPosition(), Argument.Positional.class);
+          if (arg != null) {
+            Object[] lookups = arg.getReference().getVariants();
+            for (Object lookup : lookups) {
+              if (lookup instanceof LookupElement) {
+                result.addElement((LookupElement) lookup);
+              }
+            }
+          }
+        }
+      }
+    );
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java
new file mode 100644
index 0000000..812c2b5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuildLookupElement.java
@@ -0,0 +1,108 @@
+/*
+ * 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.references.QuoteType;
+import com.intellij.codeInsight.completion.InsertionContext;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementPresentation;
+import com.intellij.openapi.editor.Document;
+import com.intellij.psi.PsiElement;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * Handles some boilerplate, and allows lazy calculation of some expensive
+ * components, which aren't required if the element is filtered out by IJ.
+ */
+public abstract class BuildLookupElement extends LookupElement {
+
+  public static final BuildLookupElement[] EMPTY_ARRAY = new BuildLookupElement[0];
+
+  protected final String baseName;
+  protected final QuoteType quoteWrapping;
+  protected final boolean wrapWithQuotes;
+
+  public BuildLookupElement(String baseName, QuoteType quoteWrapping) {
+    this.baseName = baseName;
+    this.quoteWrapping = quoteWrapping;
+    this.wrapWithQuotes = quoteWrapping != QuoteType.NoQuotes;
+  }
+
+  @Override
+  public String getLookupString() {
+    return quoteWrapping.wrap(baseName);
+  }
+
+  @Nullable
+  public abstract Icon getIcon();
+
+  protected String getItemText() {
+    return baseName;
+  }
+
+  @Nullable
+  protected String getTypeText() {
+    return null;
+  }
+
+  @Nullable
+  protected String getTailText() {
+    return null;
+  }
+
+  @Override
+  public void renderElement(LookupElementPresentation presentation) {
+    presentation.setItemText(getItemText());
+    presentation.setTailText(getTailText());
+    presentation.setTypeText(getTypeText());
+    presentation.setIcon(getIcon());
+  }
+
+  /**
+   * If we're wrapping with quotes, handle the (very common) case where we have
+   * a closing quote after the caret -- we want to remove this quote.
+   * @param context
+   */
+  @Override
+  public void handleInsert(InsertionContext context) {
+    if (!wrapWithQuotes) {
+      super.handleInsert(context);
+      return;
+    }
+    Document document = context.getDocument();
+    context.commitDocument();
+    PsiElement suffix = context.getFile().findElementAt(context.getTailOffset());
+    if (suffix.getText().startsWith(quoteWrapping.quoteString)) {
+      int offset = suffix.getTextOffset();
+      document.deleteString(offset, offset + 1);
+      context.commitDocument();
+    }
+    if (caretInsideQuotes()) {
+      context.getEditor().getCaretModel().moveCaretRelatively(-1, 0, false, false, true);
+    }
+  }
+
+  /**
+   * If true, and we're wrapping with quotes, the caret is moved inside
+   * the closing quote after the insert operation is performed.
+   */
+  protected boolean caretInsideQuotes() {
+    return false;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributor.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributor.java
new file mode 100644
index 0000000..9970994
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributor.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.completion;
+
+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.RuleDefinition;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.icons.AllIcons;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ProcessingContext;
+
+import javax.annotation.Nullable;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * Known attributes for built-in blaze functions.
+ */
+public class BuiltInFunctionAttributeCompletionContributor 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 BuiltInFunctionAttributeCompletionContributor() {
+    extend(
+      CompletionType.BASIC,
+      psiElement()
+        .withLanguage(BuildFileLanguage.INSTANCE)
+        .inside(psiElement(FuncallExpression.class))
+        .andNot(psiElement().afterLeaf("."))
+        .andOr(
+          psiElement().withSuperParent(2, FuncallExpression.class),
+          psiElement().withSuperParent(2, Argument.class)
+            .andNot(psiElement().afterLeaf("="))
+            .andNot(psiElement().afterLeaf(psiElement(BuildToken.fromKind(TokenKind.IDENTIFIER))))
+        ),
+      new CompletionProvider<CompletionParameters>() {
+        @Override
+        protected void addCompletions(CompletionParameters parameters, ProcessingContext context, CompletionResultSet result) {
+          BuildLanguageSpec spec = BuildLanguageSpecProvider.getInstance().getLanguageSpec(parameters.getPosition().getProject());
+          if (spec == null) {
+            return;
+          }
+          RuleDefinition rule = spec.getRule(getEnclosingFuncallName(parameters.getPosition()));
+          if (rule == null) {
+            return;
+          }
+          for (String attributeName : rule.getKnownAttributeNames()) {
+            result.addElement(
+              LookupElementBuilder
+                .create(attributeName)
+                .withIcon(AllIcons.Nodes.Parameter));
+          }
+        }
+      }
+    );
+  }
+
+  @Nullable
+  private static String getEnclosingFuncallName(PsiElement element) {
+    FuncallExpression funcall = PsiUtils.getParentOfType(element, FuncallExpression.class);
+    return funcall != null ? funcall.getFunctionName() : null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributor.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributor.java
new file mode 100644
index 0000000..f3bff83
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributor.java
@@ -0,0 +1,82 @@
+/*
+ * 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.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.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StatementList;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.completion.util.ParenthesesInsertHandler;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.util.ProcessingContext;
+import icons.BlazeIcons;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * Completes built-in blaze function names.
+ */
+public class BuiltInFunctionCompletionContributor 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 BuiltInFunctionCompletionContributor() {
+    extend(
+      CompletionType.BASIC,
+      psiElement()
+        .withLanguage(BuildFileLanguage.INSTANCE)
+        .andOr(
+          // Handles only top-level rules, and rules inside a function statement.
+          // There are several other possibilities (e.g. inside top-level list comprehension), but leaving out less common cases
+          // to avoid cluttering the autocomplete suggestions when it's not valid to enter a rule.
+          psiElement().withParents(ReferenceExpression.class, BuildFile.class), // leaf node => BuildReference => BuildFile
+          psiElement()
+            .inside(psiElement(StatementList.class).inside(psiElement(FunctionStatement.class)))
+            .afterLeaf(psiElement().withText(".").afterLeaf(psiElement().withText("native")))
+        ),
+      new CompletionProvider<CompletionParameters>() {
+        @Override
+        protected void addCompletions(CompletionParameters parameters, ProcessingContext context, CompletionResultSet result) {
+          BuildLanguageSpec spec = BuildLanguageSpecProvider.getInstance().getLanguageSpec(parameters.getPosition().getProject());
+          if (spec == null) {
+            return;
+          }
+          for (String ruleName : spec.getKnownRuleNames()) {
+            result.addElement(
+              LookupElementBuilder
+                .create(ruleName)
+                .withIcon(BlazeIcons.BuildRule)
+                .withInsertHandler(ParenthesesInsertHandler.getInstance(true)));
+          }
+        }
+      }
+    );
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java
new file mode 100644
index 0000000..65b0e21
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/CompletionResultsProcessor.java
@@ -0,0 +1,64 @@
+/*
+ * 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.common.collect.Maps;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.references.QuoteType;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import com.intellij.util.Processor;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * Collects completion results, removing duplicate entries.
+ */
+public class CompletionResultsProcessor implements Processor<BuildElement> {
+
+  private final Map<String, LookupElement> results = Maps.newHashMap();
+  private final PsiElement originalElement;
+  private final QuoteType quoteType;
+
+  public CompletionResultsProcessor(PsiElement originalElement, QuoteType quoteType) {
+    this.originalElement = originalElement;
+    this.quoteType = quoteType;
+  }
+
+  @Override
+  public boolean process(BuildElement buildElement) {
+    if (buildElement == originalElement) {
+      return true;
+    }
+    if (buildElement instanceof StringLiteral) {
+      StringLiteral literal = (StringLiteral)buildElement;
+      results.put(literal.getStringContents(), new StringLiteralReferenceLookupElement((StringLiteral)buildElement, quoteType));
+    }
+    else if (buildElement instanceof PsiNamedElement) {
+      PsiNamedElement namedElement = (PsiNamedElement)buildElement;
+      results.put(namedElement.getName(), new NamedBuildLookupElement((PsiNamedElement)buildElement, quoteType));
+    }
+    return true;
+  }
+
+  public Collection<LookupElement> getResults() {
+    return results.values();
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilePathLookupElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilePathLookupElement.java
new file mode 100644
index 0000000..28292e5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilePathLookupElement.java
@@ -0,0 +1,55 @@
+/*
+ * 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.references.QuoteType;
+import com.intellij.openapi.util.NullableLazyValue;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * Code completion support for package paths.
+ */
+public class FilePathLookupElement extends BuildLookupElement {
+
+  private final String itemText;
+  private final NullableLazyValue<Icon> icon;
+
+  public FilePathLookupElement(String fullLabel, String itemText, QuoteType quoteWrapping, NullableLazyValue<Icon> icon) {
+    super(fullLabel, quoteWrapping);
+    this.itemText = itemText;
+    this.icon = icon;
+  }
+
+  @Override
+  protected String getItemText() {
+    return itemText;
+  }
+
+  @Nullable
+  @Override
+  public Icon getIcon() {
+    return icon.getValue();
+  }
+
+  @Override
+  protected boolean caretInsideQuotes() {
+    // after completing, leave the caret inside the closing quote, so the user can
+    // continue typing the path.
+    return true;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.java
new file mode 100644
index 0000000..b0d9f20
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/FilterPatterns.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.base.lang.buildfile.completion;
+
+import com.intellij.patterns.ElementPattern;
+import com.intellij.patterns.PatternCondition;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.NotNull;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * Filter patterns used by completion contributors.
+ */
+public class FilterPatterns {
+
+  public static final ElementPattern<PsiElement> indexInParentsChildren(final int childIndex) {
+    return psiElement().with(new PatternCondition<PsiElement>("isIndexInParentsChildren") {
+      @Override
+      public boolean accepts(@NotNull PsiElement psiElement, ProcessingContext context) {
+        final PsiElement parent = psiElement.getParent();
+        if (parent != null) {
+          final PsiElement[] children = parent.getChildren();
+          if (childIndex < children.length  && psiElement.equals(children[childIndex])) {
+            return true;
+          }
+        }
+        return false;
+      }
+    });
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java
new file mode 100644
index 0000000..b7f0314
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/LabelRuleLookupElement.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.completion;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.references.QuoteType;
+import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Given a label fragment containing a (possibly implicit) package path,
+ * provides a lookup element to a rule target in that package.
+ */
+public class LabelRuleLookupElement extends BuildLookupElement {
+
+  public static BuildLookupElement[] collectAllRules(
+    BuildFile file,
+    String originalString,
+    String packagePrefix,
+    @Nullable String excluded,
+    QuoteType quoteType) {
+    if (packagePrefix.startsWith("//") || originalString.startsWith(":")) {
+      packagePrefix += ":";
+    }
+
+    String ruleFragment = LabelUtils.getRuleComponent(originalString);
+    List<BuildLookupElement> lookups = Lists.newArrayList();
+    // TODO: Handle rules generated via functions? (e.g. via blaze sync)
+    BuildLanguageSpec spec = BuildLanguageSpecProvider.getInstance().getLanguageSpec(file.getProject());
+    for (FuncallExpression target : file.findChildrenByClass(FuncallExpression.class)) {
+      String targetName = target.getName();
+      if (targetName == null || Objects.equals(target.getName(), excluded) || !targetName.startsWith(ruleFragment)) {
+        continue;
+      }
+      String ruleType = target.getFunctionName();
+      if (ruleType == null || (spec != null && !spec.hasRule(ruleType))) {
+        continue;
+      }
+      lookups.add(new LabelRuleLookupElement(packagePrefix, target, targetName, ruleType, quoteType));
+    }
+    return lookups.isEmpty() ? BuildLookupElement.EMPTY_ARRAY : lookups.toArray(new BuildLookupElement[lookups.size()]);
+  }
+
+  private final FuncallExpression target;
+  private final String targetName;
+  private final String ruleType;
+
+  private LabelRuleLookupElement(String packagePrefix, FuncallExpression target, String targetName, String ruleType, QuoteType quoteType) {
+    super(packagePrefix + targetName, quoteType);
+    this.target = target;
+    this.targetName = targetName;
+    this.ruleType = ruleType;
+
+    assert(packagePrefix.isEmpty() || packagePrefix.endsWith(":"));
+  }
+
+  @Override
+  public Icon getIcon() {
+    return target.getIcon(0);
+  }
+
+  @Override
+  protected String getTypeText() {
+    return ruleType;
+  }
+
+  @Override
+  protected String getItemText() {
+    return targetName;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/NamedBuildLookupElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/NamedBuildLookupElement.java
new file mode 100644
index 0000000..287e823
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/NamedBuildLookupElement.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.completion;
+
+import com.google.idea.blaze.base.lang.buildfile.references.QuoteType;
+import com.intellij.psi.PsiNamedElement;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * Generic implementation for {@link com.intellij.psi.PsiNamedElement}s
+ */
+public class NamedBuildLookupElement extends BuildLookupElement {
+
+  private final PsiNamedElement element;
+
+  public NamedBuildLookupElement(PsiNamedElement element, QuoteType quoteType) {
+    super(element.getName(), quoteType);
+    this.element = element;
+  }
+
+  @Nullable
+  @Override
+  public Icon getIcon() {
+    return element.getIcon(0);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributor.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributor.java
new file mode 100644
index 0000000..48eb0f3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributor.java
@@ -0,0 +1,55 @@
+/*
+ * 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.ParameterList;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.icons.AllIcons;
+import com.intellij.util.ProcessingContext;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * {@link CompletionContributor} for starred function parameters.
+ */
+public class ParameterCompletionContributor extends CompletionContributor {
+
+  public ParameterCompletionContributor() {
+    extend(CompletionType.BASIC,
+           psiElement().inside(ParameterList.class).afterLeaf("*"),
+           new ParameterCompletionProvider("args"));
+    extend(CompletionType.BASIC,
+           psiElement().inside(ParameterList.class).afterLeaf("**"),
+           new ParameterCompletionProvider("kwargs"));
+  }
+
+  private static class ParameterCompletionProvider extends CompletionProvider<CompletionParameters> {
+    private String myName;
+
+    private ParameterCompletionProvider(String name) {
+      myName = name;
+    }
+
+    @Override
+    protected void addCompletions(CompletionParameters parameters,
+                                  ProcessingContext context,
+                                  CompletionResultSet result) {
+      result.addElement(LookupElementBuilder.create(myName).withIcon(AllIcons.Nodes.Parameter));
+    }
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/StringLiteralReferenceLookupElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/StringLiteralReferenceLookupElement.java
new file mode 100644
index 0000000..0d8f3fe
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/completion/StringLiteralReferenceLookupElement.java
@@ -0,0 +1,53 @@
+/*
+ * 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.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.references.QuoteType;
+import com.intellij.openapi.util.NullableLazyValue;
+import com.intellij.psi.PsiElement;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * We calculate the referenced element lazily, as it often won't be needed
+ * (e.g. when the string doesn't match the string fragment being completed.
+ */
+public class StringLiteralReferenceLookupElement extends BuildLookupElement {
+
+  private final StringLiteral literal;
+  private NullableLazyValue<PsiElement> referencedElement = new NullableLazyValue<PsiElement>() {
+    @Nullable
+    @Override
+    protected PsiElement compute() {
+      return literal.getReferencedElement();
+    }
+  };
+
+  public StringLiteralReferenceLookupElement(StringLiteral literal, QuoteType quoteType) {
+    super(literal.getStringContents(), quoteType);
+    this.literal = literal;
+  }
+
+  @Nullable
+  @Override
+  public Icon getIcon() {
+    PsiElement ref = referencedElement.getValue();
+    return ref != null ? ref.getIcon(0) : null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildEnterBetweenBracketsHandler.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildEnterBetweenBracketsHandler.java
new file mode 100644
index 0000000..3231550
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildEnterBetweenBracketsHandler.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.lang.buildfile.editor;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.codeInsight.editorActions.enter.EnterBetweenBracesHandler;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Extends IntelliJ's {@link EnterBetweenBracesHandler}, including square brackets as a valid
+ * brace pair type.
+ */
+public class BuildEnterBetweenBracketsHandler extends EnterBetweenBracesHandler {
+  @Override
+  public Result preprocessEnter(@NotNull PsiFile file,
+                                @NotNull Editor editor,
+                                @NotNull Ref<Integer> caretOffsetRef,
+                                @NotNull Ref<Integer> caretAdvance,
+                                @NotNull DataContext dataContext,
+                                EditorActionHandler originalHandler) {
+    if (!(file instanceof BuildFile)) {
+      return Result.Continue;
+    }
+    return super.preprocessEnter(file, editor, caretOffsetRef, caretAdvance, dataContext, originalHandler);
+  }
+
+  @Override
+  protected boolean isBracePair(char c1, char c2) {
+    return c1 == '[' && c2  == ']';
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildEnterHandler.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildEnterHandler.java
new file mode 100644
index 0000000..044280c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildEnterHandler.java
@@ -0,0 +1,251 @@
+/*
+ * 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.editor;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
+import com.intellij.ide.DataManager;
+import com.intellij.injected.editor.EditorWindow;
+import com.intellij.lang.injection.InjectedLanguageManager;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
+import com.intellij.openapi.editor.actions.SplitLineAction;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.*;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CodeStyleSettingsManager;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings.IndentOptions;
+import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
+import com.intellij.util.text.CharArrayUtil;
+
+import javax.annotation.Nullable;
+
+/**
+ * Inserts indents as appropriate when enter is pressed.<br>
+ * This is a substitute for implementing a full FormattingModel for the BUILD language.
+ * If we ever decide to do that, this code should be removed.
+ */
+public class BuildEnterHandler extends EnterHandlerDelegateAdapter {
+
+  @Override
+  public Result preprocessEnter(PsiFile file,
+                                Editor editor,
+                                Ref<Integer> caretOffset,
+                                Ref<Integer> caretAdvance,
+                                DataContext dataContext,
+                                EditorActionHandler originalHandler) {
+    int offset = caretOffset.get();
+    if (editor instanceof EditorWindow) {
+      file = InjectedLanguageManager.getInstance(file.getProject()).getTopLevelFile(file);
+      editor = InjectedLanguageUtil.getTopLevelEditor(editor);
+      offset = editor.getCaretModel().getOffset();
+    }
+    if (!isApplicable(file, dataContext)) {
+      return Result.Continue;
+    }
+
+    // Previous enter handler's (e.g. EnterBetweenBracesHandler) can introduce a mismatch
+    // between the editor's caret model and the offset we've been provided with.
+    editor.getCaretModel().moveToOffset(offset);
+
+    Document doc = editor.getDocument();
+    PsiDocumentManager.getInstance(file.getProject()).commitDocument(doc);
+
+    CodeStyleSettings currentSettings = CodeStyleSettingsManager.getSettings(file.getProject());
+    IndentOptions indentOptions = currentSettings.getIndentOptions(file.getFileType());
+
+    Integer indent = determineIndent(file, editor, offset, indentOptions);
+    if (indent == null) {
+      return Result.Continue;
+    }
+
+    removeTrailingWhitespace(doc, file, offset);
+    originalHandler.execute(editor, editor.getCaretModel().getCurrentCaret(), dataContext);
+    LogicalPosition position = editor.getCaretModel().getLogicalPosition();
+    if (position.column == indent) {
+      return Result.Stop;
+    }
+    if (position.column > indent) {
+      //default enter handler has added too many spaces -- remove them
+      int excess = position.column - indent;
+      doc.deleteString(editor.getCaretModel().getOffset() - excess, editor.getCaretModel().getOffset());
+    } else if (position.column < indent) {
+      String spaces = StringUtil.repeatSymbol(' ', indent - position.column);
+      doc.insertString(editor.getCaretModel().getOffset(), spaces);
+    }
+    editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(position.line, indent));
+    return Result.Stop;
+  }
+
+  private static void removeTrailingWhitespace(Document doc, PsiFile file, int offset) {
+    CharSequence chars = doc.getCharsSequence();
+    int start = offset;
+    while (offset < chars.length() && chars.charAt(offset) == ' ') {
+      PsiElement element = file.findElementAt(offset);
+      if (element == null || !(element instanceof PsiWhiteSpace)) {
+        break;
+      }
+      offset++;
+    }
+    if (start != offset) {
+      doc.deleteString(start, offset);
+    }
+  }
+
+  private static boolean isApplicable(PsiFile file, DataContext dataContext) {
+    if (!(file instanceof BuildFile)) {
+      return false;
+    }
+    Boolean isSplitLine = DataManager.getInstance().loadFromDataContext(dataContext, SplitLineAction.SPLIT_LINE_KEY);
+    if (isSplitLine != null) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Returns null if an appropriate indent cannot be found. In that case we do nothing,
+   * and pass it along to the next EnterHandler.
+   */
+  @Nullable
+  private static Integer determineIndent(PsiFile file, Editor editor, int offset, IndentOptions indentOptions) {
+    if (offset == 0) {
+      return null;
+    }
+    Document doc = editor.getDocument();
+    PsiElement element = getRelevantElement(file, doc, offset);
+    PsiElement parent = element != null ? element.getParent() : null;
+    if (parent == null) {
+      return null;
+    }
+    if (endsBlock(element)) {
+      // current line indent subtract block indent
+      return Math.max(0, getIndent(doc, element) - indentOptions.INDENT_SIZE);
+    }
+
+    if (parent instanceof BuildListType) {
+      BuildListType list = (BuildListType) parent;
+      int listOffset = list.getStartOffset();
+      LogicalPosition caretPosition = editor.getCaretModel().getLogicalPosition();
+      LogicalPosition listStart = editor.offsetToLogicalPosition(listOffset);
+      if (listStart.line != caretPosition.line) {
+        // take the minimum of the current line's indent and the current caret position
+        return indentOfLineUpToCaret(doc, caretPosition.line, offset);
+      }
+      BuildElement firstChild = ((BuildListType) parent).getFirstElement();
+      if (firstChild != null && firstChild.getNode().getStartOffset() < offset) {
+        return getIndent(doc, firstChild);
+      }
+      return lineIndent(doc, listStart.line) + additionalIndent(parent, indentOptions);
+    }
+    if (parent instanceof StatementListContainer && afterColon(doc, offset)) {
+      return getIndent(doc, parent) + additionalIndent(parent, indentOptions);
+    }
+    return null;
+  }
+
+  private static int additionalIndent(PsiElement parent, IndentOptions indentOptions) {
+    return parent instanceof StatementListContainer
+      ? indentOptions.INDENT_SIZE : indentOptions.CONTINUATION_INDENT_SIZE;
+  }
+
+  private static int lineIndent(Document doc, int line) {
+    int startOffset = doc.getLineStartOffset(line);
+    int indentOffset = CharArrayUtil.shiftForward(doc.getCharsSequence(), startOffset, " \t");
+    return indentOffset - startOffset;
+  }
+
+  private static int getIndent(Document doc, PsiElement element) {
+    int offset = element.getNode().getStartOffset();
+    int lineNumber = doc.getLineNumber(offset);
+    return offset - doc.getLineStartOffset(lineNumber);
+  }
+
+  private static int indentOfLineUpToCaret(Document doc, int line, int caretOffset) {
+    int startOffset = doc.getLineStartOffset(line);
+    int indentOffset = CharArrayUtil.shiftForward(doc.getCharsSequence(), startOffset, " \t");
+    return Math.min(indentOffset, caretOffset) - startOffset;
+  }
+
+  private static boolean endsBlock(PsiElement element) {
+    return element instanceof ReturnStatement
+      || element instanceof PassStatement;
+  }
+
+  private static PsiElement getBlockEndingParent(PsiElement element) {
+    while (element != null && !(element instanceof PsiFileSystemItem)) {
+      if (endsBlock(element)) {
+        return element;
+      }
+      element = element.getParent();
+    }
+    return null;
+  }
+
+  @Nullable
+  private static PsiElement getRelevantElement(PsiFile file, Document doc, int offset) {
+    if (offset == 0) {
+      return null;
+    }
+    if (offset == doc.getTextLength()) {
+      offset--;
+    }
+    PsiElement element = file.findElementAt(offset);
+    while (element != null && isWhiteSpace(element)) {
+      element = PsiUtils.getPreviousNodeInTree(element);
+    }
+    PsiElement blockTerminator = getBlockEndingParent(element);
+    if (blockTerminator != null
+        && blockTerminator.getTextRange().getEndOffset() == element.getTextRange().getEndOffset()) {
+      return blockTerminator;
+    }
+    while (element != null && skipElement(element, offset)) {
+      element = element.getParent();
+    }
+    return element;
+  }
+
+  private static boolean isWhiteSpace(PsiElement element) {
+    if (element instanceof PsiWhiteSpace) {
+      return true;
+    }
+    return BuildToken.WHITESPACE_AND_NEWLINE.contains(element.getNode().getElementType());
+  }
+
+  private static boolean skipElement(PsiElement element, int offset) {
+    PsiElement parent = element.getParent();
+    if (parent == null || parent.getNode() == null || parent instanceof PsiFileSystemItem) {
+      return false;
+    }
+    TextRange childRange = element.getNode().getTextRange();
+    return childRange.equals(parent.getNode().getTextRange())
+      || childRange.getStartOffset() == offset && (parent instanceof Argument || parent instanceof Parameter);
+  }
+
+  private static boolean afterColon(Document doc, int offset) {
+    CharSequence text = doc.getCharsSequence();
+    int previousOffset = CharArrayUtil.shiftBackward(text, offset - 1, " \t");
+    return text.charAt(previousOffset) == ':';
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandler.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandler.java
new file mode 100644
index 0000000..ecde586
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandler.java
@@ -0,0 +1,142 @@
+/*
+ * 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.editor;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.intellij.codeInsight.editorActions.MultiCharQuoteHandler;
+import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.highlighter.HighlighterIterator;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.tree.IElementType;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides quote auto-closing support.
+ */
+public class BuildQuoteHandler extends SimpleTokenSetQuoteHandler implements MultiCharQuoteHandler {
+
+  public BuildQuoteHandler() {
+    super(BuildToken.fromKind(TokenKind.STRING));
+  }
+
+  @Override
+  public boolean isOpeningQuote(HighlighterIterator iterator, int offset) {
+    if (!myLiteralTokenSet.contains(iterator.getTokenType())) {
+      return false;
+    }
+    int start = iterator.getStart();
+    if (offset == start) {
+      return true;
+    }
+    final Document document = iterator.getDocument();
+    if (document == null) {
+      return false;
+    }
+    CharSequence text = document.getCharsSequence();
+    char theQuote = text.charAt(offset);
+    if (offset >= 2 &&
+        text.charAt(offset - 1) == theQuote &&
+        text.charAt(offset - 2) == theQuote &&
+        (offset < 3 || text.charAt(offset - 3) != theQuote))
+    if (super.isOpeningQuote(iterator, offset)) {
+      return true;
+    }
+    return false;
+  }
+
+  private static int getLiteralStartOffset(CharSequence text, int start) {
+    char c = Character.toUpperCase(text.charAt(start));
+    if (c == 'U' || c == 'B') {
+      start++;
+      c = Character.toUpperCase(text.charAt(start));
+    }
+    if (c == 'R') {
+      start++;
+    }
+    return start;
+  }
+
+  @Override
+  protected boolean isNonClosedLiteral(HighlighterIterator iterator, CharSequence chars) {
+    int end = iterator.getEnd();
+    if (getLiteralStartOffset(chars, iterator.getStart()) >= end - 1) {
+      return true;
+    }
+    char endSymbol = chars.charAt(end - 1);
+    if (endSymbol != '"' && endSymbol != '\'') {
+      return true;
+    }
+
+    //for triple quoted string
+    if (end >= 3 &&
+        (endSymbol == chars.charAt(end - 2)) && (chars.charAt(end - 2) == chars.charAt(end - 3)) &&
+        (end < 4 || chars.charAt(end - 4) != endSymbol)) {
+      return true;
+    }
+
+    return false;
+  }
+
+  @Override
+  public boolean isClosingQuote(HighlighterIterator iterator, int offset) {
+    final IElementType tokenType = iterator.getTokenType();
+    if (!myLiteralTokenSet.contains(tokenType)) {
+      return false;
+    }
+    int start = iterator.getStart();
+    int end = iterator.getEnd();
+    if (end - start >= 1 && offset == end - 1) {
+      return true; // single quote
+    }
+    if (end - start < 3 || offset < end - 3) {
+      return false;
+    }
+    // check for triple quote
+    Document doc = iterator.getDocument();
+    if (doc == null) {
+      return false;
+    }
+    CharSequence chars = doc.getCharsSequence();
+    char quote = chars.charAt(start);
+    boolean tripleQuote = quote == chars.charAt(start + 1)
+      && quote == chars.charAt(start + 2);
+    if (!tripleQuote) {
+      return false;
+    }
+    for (int i = offset; i < Math.min(offset + 2, end); i++) {
+      if (quote != chars.charAt(i)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Nullable
+  @Override
+  public CharSequence getClosingQuote(HighlighterIterator iterator, int offset) {
+    char theQuote = iterator.getDocument().getCharsSequence().charAt(offset - 1);
+    if (super.isOpeningQuote(iterator, offset - 1)) {
+      return String.valueOf(theQuote);
+    }
+    if (super.isOpeningQuote(iterator, offset - 3)) {
+      return StringUtil.repeat(String.valueOf(theQuote), 3);
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildElementDescriptionProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildElementDescriptionProvider.java
new file mode 100644
index 0000000..817a158
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildElementDescriptionProvider.java
@@ -0,0 +1,50 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
+import com.intellij.psi.ElementDescriptionLocation;
+import com.intellij.psi.ElementDescriptionProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNameIdentifierOwner;
+import com.intellij.usageView.UsageViewLongNameLocation;
+import com.intellij.usageView.UsageViewShortNameLocation;
+
+import javax.annotation.Nullable;
+
+/**
+ * Controls text shown for target in the 'find usages' dialog.
+ */
+public class BuildElementDescriptionProvider implements ElementDescriptionProvider {
+  @Nullable
+  @Override
+  public String getElementDescription(PsiElement element, ElementDescriptionLocation location) {
+    if (!(element instanceof BuildElement)) {
+      return null;
+    }
+    if (location instanceof UsageViewLongNameLocation) {
+      return ((BuildElement) element).getPresentableText();
+    }
+    if (location instanceof UsageViewShortNameLocation) {
+      if (element instanceof PsiNameIdentifierOwner) {
+        // this is used by rename operations, so needs to be accurate
+        return ((PsiNameIdentifierOwner) element).getName();
+      }
+      return ((BuildElement) element).getPresentableText();
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFileGroupingRuleProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFileGroupingRuleProvider.java
new file mode 100644
index 0000000..73a9d4f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFileGroupingRuleProvider.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.usages.Usage;
+import com.intellij.usages.UsageGroup;
+import com.intellij.usages.UsageView;
+import com.intellij.usages.impl.FileStructureGroupRuleProvider;
+import com.intellij.usages.impl.rules.FileGroupingRule;
+import com.intellij.usages.rules.UsageGroupingRule;
+import com.intellij.usages.rules.UsageInFile;
+
+import javax.swing.*;
+
+/**
+ * Allows us to customize the filename string in the 'find usages' dialog, rather than displaying them all as 'BUILD'.
+ */
+public class BuildFileGroupingRuleProvider implements FileStructureGroupRuleProvider {
+
+  public static UsageGroupingRule getGroupingRule(Project project) {
+    return new BuildFileGroupingRule(project);
+  }
+
+  @Override
+  public UsageGroupingRule getUsageGroupingRule(Project project) {
+    return getGroupingRule(project);
+  }
+
+  private static class BuildFileGroupingRule extends FileGroupingRule {
+
+    private final Project project;
+
+    BuildFileGroupingRule(Project project) {
+      super(project);
+      this.project = project;
+    }
+
+    @Override
+    public UsageGroup groupUsage(Usage usage) {
+      if (!(usage instanceof UsageInFile)) {
+        return null;
+      }
+      final VirtualFile virtualFile = ((UsageInFile) usage).getFile();
+      if (virtualFile.getFileType() != BuildFileType.INSTANCE) {
+        return null;
+      }
+      return new FileUsageGroup(project, virtualFile) {
+        String name;
+
+        @Override
+        public void update() {
+          if (isValid()) {
+            super.update();
+            name = BuildFile.getBuildFileString(project, virtualFile.getPath());
+          }
+        }
+
+        @Override
+        public String getPresentableName() {
+          return name;
+        }
+
+        @Override
+        public String getText(UsageView view) {
+          return name;
+        }
+
+        @Override
+        public Icon getIcon(boolean isOpen) {
+          return null; // already shown by default usage group (which we can't remove...)
+        }
+      };
+    }
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFindUsagesProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFindUsagesProvider.java
new file mode 100644
index 0000000..792bda8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildFindUsagesProvider.java
@@ -0,0 +1,107 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexer;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexerBase.LexerMode;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.intellij.lang.HelpID;
+import com.intellij.lang.cacheBuilder.DefaultWordsScanner;
+import com.intellij.lang.cacheBuilder.WordsScanner;
+import com.intellij.lang.findUsages.FindUsagesProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import com.intellij.psi.tree.TokenSet;
+
+/**
+ * Required for highlighting references (among other things we don't currently support).
+ * Currently only used by the fallback 'DefaultFindUsagesHandlerFactory'.
+ */
+public class BuildFindUsagesProvider implements FindUsagesProvider {
+
+  @Override
+  public boolean canFindUsagesFor(PsiElement psiElement) {
+    return psiElement instanceof FuncallExpression
+      || psiElement instanceof PsiNamedElement
+      || psiElement instanceof ReferenceExpression;
+  }
+
+  @Override
+  public String getHelpId(PsiElement psiElement) {
+    if (psiElement instanceof FunctionStatement) {
+      return "reference.dialogs.findUsages.method";
+    }
+    if (psiElement instanceof TargetExpression
+      || psiElement instanceof Parameter
+      || psiElement instanceof ReferenceExpression) {
+      return "reference.dialogs.findUsages.variable";
+    }
+    // typically build rules and imported Skylark functions, but also all other function calls
+    return HelpID.FIND_OTHER_USAGES;
+  }
+
+  @Override
+  public String getType(PsiElement element) {
+    if (element instanceof FunctionStatement) {
+      return "function";
+    }
+    if (element instanceof Parameter) {
+      return "parameter";
+    }
+    if (element instanceof ReferenceExpression
+      || element instanceof TargetExpression) {
+      return "variable";
+    }
+    if (element instanceof Argument.Keyword) {
+      return "keyword argument";
+    }
+    if (element instanceof FuncallExpression) {
+      return "rule";
+    }
+    return "";
+  }
+
+  /**
+   * Controls text shown for target element in the 'find usages' dialog
+   */
+  @Override
+  public String getDescriptiveName(PsiElement element) {
+    if (element instanceof BuildElement) {
+      return ((BuildElement) element).getPresentableText();
+    }
+    return element.toString();
+  }
+
+  @Override
+  public String getNodeText(PsiElement element, boolean useFullName) {
+    return getDescriptiveName(element);
+  }
+
+  @Override
+  public WordsScanner getWordsScanner() {
+    return new DefaultWordsScanner(
+      new BuildLexer(LexerMode.SyntaxHighlighting),
+      tokenSet(TokenKind.IDENTIFIER),
+      tokenSet(TokenKind.COMMENT),
+      tokenSet(TokenKind.STRING));
+  }
+
+  private static TokenSet tokenSet(TokenKind token) {
+    return TokenSet.create(BuildToken.fromKind(token));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildReadWriteAccessDetector.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildReadWriteAccessDetector.java
new file mode 100644
index 0000000..91d6810
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildReadWriteAccessDetector.java
@@ -0,0 +1,57 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.AugmentedAssignmentStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.TargetExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.codeInsight.highlighting.ReadWriteAccessDetector;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+
+/**
+ * Used by find usages tools.
+ */
+public class BuildReadWriteAccessDetector extends ReadWriteAccessDetector {
+  @Override
+  public boolean isReadWriteAccessible(PsiElement element) {
+    return element instanceof TargetExpression || element instanceof ReferenceExpression;
+  }
+
+  @Override
+  public boolean isDeclarationWriteAccess(PsiElement element) {
+    return element instanceof TargetExpression;
+  }
+
+  @Override
+  public Access getReferenceAccess(PsiElement referencedElement, PsiReference reference) {
+    return getExpressionAccess(reference.getElement());
+  }
+
+  @Override
+  public Access getExpressionAccess(PsiElement expression) {
+    if (isDeclarationWriteAccess(expression)) {
+      return Access.Write;
+    }
+    if (expression instanceof ReferenceExpression) {
+      if (PsiUtils.getParentOfType(expression, AugmentedAssignmentStatement.class) != null) {
+        return Access.ReadWrite;
+      }
+    }
+    return Access.Read;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java
new file mode 100644
index 0000000..8c0ca79
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildTargetElementEvaluator.java
@@ -0,0 +1,109 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument.Keyword;
+import com.google.idea.blaze.base.lang.buildfile.psi.ArgumentList;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.intellij.codeInsight.TargetElementEvaluatorEx2;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.source.resolve.reference.impl.PsiMultiReference;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Objects;
+
+/**
+ * StringLiterals can reference multiple targets (e.g. "package:target" references both the package and the target).
+ * IntelliJ defaults to highlighting / navigating to the innermost reference, but in this case, we want the
+ * opposite behavior (the target reference should trump the package reference).
+ */
+public class BuildTargetElementEvaluator extends TargetElementEvaluatorEx2 {
+
+  @Override
+  public boolean includeSelfInGotoImplementation(PsiElement element) {
+    return false;
+  }
+
+  /**
+   * Returns null in the cases where we're happy with the default behavior.
+   */
+  @Nullable
+  @Override
+  public PsiElement getElementByReference(PsiReference ref, int flags) {
+    if (!(ref instanceof PsiMultiReference) || !(ref.getElement() instanceof StringLiteral)) {
+      return null;
+    }
+    // choose the outer-most reference
+    PsiReference[] refs = ((PsiMultiReference) ref).getReferences().clone();
+    Arrays.sort(refs, COMPARATOR);
+    return refs[0].resolve();
+  }
+
+  private static final Comparator<PsiReference> COMPARATOR = new Comparator<PsiReference>() {
+    @Override
+    public int compare(final PsiReference ref1, final PsiReference ref2) {
+      boolean resolves1 = ref1.resolve() != null;
+      boolean resolves2 = ref2.resolve() != null;
+      if (resolves1 && !resolves2) return -1;
+      if (!resolves1 && resolves2) return 1;
+
+      final TextRange range1 = ref1.getRangeInElement();
+      final TextRange range2 = ref2.getRangeInElement();
+
+      if(TextRange.areSegmentsEqual(range1, range2)) return 0;
+      if(range1.getStartOffset() >= range2.getStartOffset() && range1.getEndOffset() <= range2.getEndOffset()) return 1;
+      if(range2.getStartOffset() >= range1.getStartOffset() && range2.getEndOffset() <= range1.getEndOffset()) return -1;
+
+      return 0;
+    }
+  };
+
+  /**
+   * Redirect 'name' funcall argument values to the funcall expression (b/29088829).
+   */
+  @Nullable
+  @Override
+  public PsiElement getNamedElement(PsiElement element) {
+    return getParentFuncallIfNameString(element);
+  }
+
+  @Nullable
+  private static FuncallExpression getParentFuncallIfNameString(PsiElement element) {
+    PsiElement parent = element.getParent();
+    if (!(parent instanceof StringLiteral)) {
+      return null;
+    }
+    parent = parent.getParent();
+    if (!(parent instanceof Keyword)) {
+      return null;
+    }
+    if (!Objects.equals(((Keyword) parent).getName(), "name")) {
+      return null;
+    }
+    parent = parent.getParent();
+    if (!(parent instanceof ArgumentList)) {
+      return null;
+    }
+    parent = parent.getParent();
+    return parent instanceof FuncallExpression ? (FuncallExpression) parent : null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildUsageGroupingRuleProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildUsageGroupingRuleProvider.java
new file mode 100644
index 0000000..9371a92
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/findusages/BuildUsageGroupingRuleProvider.java
@@ -0,0 +1,39 @@
+/*
+ * 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.findusages;
+
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.project.Project;
+import com.intellij.usages.UsageView;
+import com.intellij.usages.rules.UsageGroupingRule;
+import com.intellij.usages.rules.UsageGroupingRuleProvider;
+
+/**
+ * This is a gross hack. We want to always include file paths for BUILD files in the 'find usages' dialog.<br>
+ * This achieves that by inserting an additional UsageGroupingRule for each file usage,
+ * regardless of whether we're grouping by file structure
+ */
+public class BuildUsageGroupingRuleProvider implements UsageGroupingRuleProvider {
+  @Override
+  public UsageGroupingRule[] getActiveRules(Project project) {
+    return new UsageGroupingRule[] {BuildFileGroupingRuleProvider.getGroupingRule(project)};
+  }
+
+  @Override
+  public AnAction[] createGroupingActions(UsageView view) {
+    return new AnAction[0];
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildBraceMatcher.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildBraceMatcher.java
new file mode 100644
index 0000000..29f9528
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildBraceMatcher.java
@@ -0,0 +1,75 @@
+/*
+ * 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.formatting;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.intellij.lang.BracePair;
+import com.intellij.lang.PairedBraceMatcher;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.TokenSet;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+
+/**
+ * This adds a close brace automatically once an opening brace is typed by the user in the editor.
+ */
+public class BuildBraceMatcher implements PairedBraceMatcher {
+
+  private static final BracePair[] PAIRS = new BracePair[] {
+    new BracePair(BuildToken.fromKind(TokenKind.LPAREN), BuildToken.fromKind(TokenKind.RPAREN), true),
+    new BracePair(BuildToken.fromKind(TokenKind.LBRACKET), BuildToken.fromKind(TokenKind.RBRACKET), true),
+    new BracePair(BuildToken.fromKind(TokenKind.LBRACE), BuildToken.fromKind(TokenKind.RBRACE), true)
+  };
+
+  private static final TokenSet BRACES_ALLOWED_BEFORE = tokenSet(
+    TokenKind.NEWLINE,
+    TokenKind.WHITESPACE,
+    TokenKind.COMMENT,
+    TokenKind.COLON,
+    TokenKind.COMMA,
+    TokenKind.RPAREN,
+    TokenKind.RBRACKET,
+    TokenKind.RBRACE,
+    TokenKind.LBRACE
+  );
+
+  @Override
+  public BracePair[] getPairs() {
+    return PAIRS;
+  }
+
+  @Override
+  public boolean isPairedBracesAllowedBeforeType(IElementType lbraceType, @Nullable IElementType contextType) {
+    return contextType == null || BRACES_ALLOWED_BEFORE.contains(contextType);
+  }
+
+  @Override
+  public int getCodeConstructStart(PsiFile file, int openingBraceOffset) {
+    return openingBraceOffset;
+  }
+
+  private static TokenSet tokenSet(TokenKind... kind) {
+    return TokenSet.create(
+      Arrays.stream(kind)
+        .map(BuildToken::fromKind)
+        .toArray(IElementType[]::new)
+    );
+  }
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCodeStyleSettingsProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCodeStyleSettingsProvider.java
new file mode 100644
index 0000000..ac31695
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCodeStyleSettingsProvider.java
@@ -0,0 +1,62 @@
+/*
+ * 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.formatting;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.intellij.application.options.CodeStyleAbstractConfigurable;
+import com.intellij.application.options.CodeStyleAbstractPanel;
+import com.intellij.application.options.TabbedLanguageCodeStylePanel;
+import com.intellij.openapi.options.Configurable;
+import com.intellij.psi.codeStyle.CodeStyleSettings;
+import com.intellij.psi.codeStyle.CodeStyleSettingsProvider;
+import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider;
+
+import javax.annotation.Nullable;
+
+/**
+ * Separate configurable code-style settings for BUILD language.
+ */
+public class BuildCodeStyleSettingsProvider extends CodeStyleSettingsProvider {
+
+  @Override
+  public Configurable createSettingsPage(CodeStyleSettings settings, CodeStyleSettings originalSettings) {
+    return new CodeStyleAbstractConfigurable(settings, originalSettings, BuildFileType.INSTANCE.getDescription()) {
+      @Override
+      protected CodeStyleAbstractPanel createPanel(final CodeStyleSettings settings) {
+        return new TabbedLanguageCodeStylePanel(BuildFileLanguage.INSTANCE, getCurrentSettings(), settings) {
+          @Override
+          protected void initTabs(CodeStyleSettings settings) {
+            LanguageCodeStyleSettingsProvider provider = LanguageCodeStyleSettingsProvider.forLanguage(getDefaultLanguage());
+            addIndentOptionsTab(settings);
+          }
+        };
+      }
+
+      @Override
+      public String getHelpTopic() {
+        return null;
+      }
+    };
+  }
+
+  @Nullable
+  @Override
+  public String getConfigurableDisplayName() {
+    return BuildFileType.INSTANCE.getDescription();
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java
new file mode 100644
index 0000000..ad9d849
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildCommenter.java
@@ -0,0 +1,95 @@
+/*
+ * 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.formatting;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.intellij.lang.CodeDocumentationAwareCommenter;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Supports (un)commenting lines via IntelliJ
+ */
+public class BuildCommenter implements CodeDocumentationAwareCommenter {
+
+  @Nullable
+  @Override
+  public String getLineCommentPrefix() {
+    return "#";
+  }
+
+  @Nullable
+  @Override
+  public String getBlockCommentPrefix() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getBlockCommentSuffix() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getCommentedBlockCommentPrefix() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getCommentedBlockCommentSuffix() {
+    return null;
+  }
+
+  @Override
+  public IElementType getLineCommentTokenType() {
+    return BuildToken.fromKind(TokenKind.COMMENT);
+  }
+
+  @Override
+  public IElementType getBlockCommentTokenType() {
+    return null;
+  }
+
+  @Override
+  public IElementType getDocumentationCommentTokenType() {
+    return null;
+  }
+
+  @Override
+  public String getDocumentationCommentPrefix() {
+    return null;
+  }
+
+  @Override
+  public String getDocumentationCommentLinePrefix() {
+    return null;
+  }
+
+  @Override
+  public String getDocumentationCommentSuffix() {
+    return null;
+  }
+
+  @Override
+  public boolean isDocumentationComment(PsiComment element) {
+    return false;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilder.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilder.java
new file mode 100644
index 0000000..d17ebfe
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilder.java
@@ -0,0 +1,144 @@
+/*
+ * 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.formatting;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementTypes;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.FileASTNode;
+import com.intellij.lang.folding.FoldingBuilder;
+import com.intellij.lang.folding.FoldingDescriptor;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.TokenType;
+import com.intellij.psi.tree.IElementType;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Simple code block folding for BUILD files.
+ */
+public class BuildFileFoldingBuilder implements FoldingBuilder {
+
+  /**
+   * Currently only folding top-level nodes.
+   */
+  @Override
+  public FoldingDescriptor[] buildFoldRegions(ASTNode node, Document document) {
+    List<FoldingDescriptor> descriptors = Lists.newArrayList();
+    if (node instanceof FileASTNode) {
+      for (ASTNode child : node.getChildren(null)) {
+        addDescriptors(descriptors, child);
+      }
+    } else if (isTopLevel(node)) {
+      addDescriptors(descriptors, node);
+    }
+    return descriptors.toArray(new FoldingDescriptor[0]);
+  }
+
+  /**
+   * Only folding top-level statements
+   */
+  private void addDescriptors(List<FoldingDescriptor> descriptors, ASTNode node) {
+    IElementType type = node.getElementType();
+    if (type == BuildElementTypes.FUNCTION_STATEMENT) {
+      ASTNode colon = node.findChildByType(BuildToken.fromKind(TokenKind.COLON));
+      if (colon == null) {
+        return;
+      }
+      ASTNode stmtList = node.findChildByType(BuildElementTypes.STATEMENT_LIST);
+      if (stmtList == null) {
+        return;
+      }
+      int start = colon.getStartOffset() + 1;
+      int end = endOfList(stmtList);
+      descriptors.add(new FoldingDescriptor(node, range(start, end)));
+
+    } else if (type == BuildElementTypes.FUNCALL_EXPRESSION || type == BuildElementTypes.LOAD_STATEMENT) {
+      ASTNode listNode = type == BuildElementTypes.FUNCALL_EXPRESSION ? node.findChildByType(BuildElementTypes.ARGUMENT_LIST) : node;
+      if (listNode == null) {
+        return;
+      }
+      ASTNode lParen = listNode.findChildByType(BuildToken.fromKind(TokenKind.LPAREN));
+      ASTNode rParen = listNode.findChildByType(BuildToken.fromKind(TokenKind.RPAREN));
+      if (lParen == null || rParen == null) {
+        return;
+      }
+      int start = lParen.getStartOffset() + 1;
+      int end = rParen.getTextRange().getEndOffset() - 1;
+      descriptors.add(new FoldingDescriptor(node, range(start, end)));
+    }
+  }
+
+  private static TextRange range(int start, int end) {
+    if (start >= end) {
+      return new TextRange(start, start + 1);
+    }
+    return new TextRange(start, end);
+  }
+
+  /**
+   * Don't include whitespace and newlines at the end of the function.<br>
+   * Could do this in the lexer instead, with additional look-ahead checks.
+   */
+  private int endOfList(ASTNode stmtList) {
+    ASTNode child = stmtList.getLastChildNode();
+    while (child != null) {
+      IElementType type = child.getElementType();
+      if (type != TokenType.WHITE_SPACE
+        && type != BuildToken.fromKind(TokenKind.NEWLINE)) {
+        return child.getTextRange().getEndOffset();
+      }
+      child = child.getTreePrev();
+    }
+    return stmtList.getTextRange().getEndOffset();
+  }
+
+  private boolean isTopLevel(ASTNode node) {
+    return node.getTreeParent() instanceof FileASTNode;
+  }
+
+  @Override
+  @Nullable
+  public String getPlaceholderText(ASTNode node) {
+    PsiElement psi = node.getPsi();
+    if (psi instanceof FuncallExpression) {
+      FuncallExpression expr = (FuncallExpression) psi;
+      String name = expr.getNameArgumentValue();
+      if (name != null) {
+        return "name = \"" + name + "\"...";
+      }
+    }
+    if (psi instanceof LoadStatement) {
+      String fileName = ((LoadStatement) psi).getImportedPath();
+      if (fileName != null) {
+        return "\"" + fileName + "\"...";
+      }
+    }
+    return "...";
+  }
+
+  @Override
+  public boolean isCollapsedByDefault(ASTNode node) {
+    return false;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java
new file mode 100644
index 0000000..3300a97
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/formatting/BuildLanguageCodeStyleSettingsProvider.java
@@ -0,0 +1,57 @@
+/*
+ * 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.formatting;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
+import com.intellij.application.options.IndentOptionsEditor;
+import com.intellij.application.options.SmartIndentOptionsEditor;
+import com.intellij.lang.Language;
+import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
+import com.intellij.psi.codeStyle.LanguageCodeStyleSettingsProvider;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Allows BUILD language-specific code style settings
+ */
+public class BuildLanguageCodeStyleSettingsProvider extends LanguageCodeStyleSettingsProvider {
+
+  @Override
+  public Language getLanguage() {
+    return BuildFileLanguage.INSTANCE;
+  }
+
+  @Override
+  public IndentOptionsEditor getIndentOptionsEditor() {
+    return new SmartIndentOptionsEditor();
+  }
+
+  @Override
+  public String getCodeSample(@NotNull SettingsType settingsType) {
+    return "";
+  }
+
+  @Nullable
+  @Override
+  public CommonCodeStyleSettings getDefaultCommonSettings() {
+    CommonCodeStyleSettings defaultSettings = new CommonCodeStyleSettings(BuildFileLanguage.INSTANCE);
+    CommonCodeStyleSettings.IndentOptions indentOptions = defaultSettings.initIndentOptions();
+    indentOptions.TAB_SIZE = 2;
+    indentOptions.INDENT_SIZE = 2;
+    indentOptions.CONTINUATION_INDENT_SIZE = 4;
+    return defaultSettings;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/globbing/UnixGlob.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/globbing/UnixGlob.java
new file mode 100644
index 0000000..6a0200d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/globbing/UnixGlob.java
@@ -0,0 +1,705 @@
+/*
+ * 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.globbing;
+
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.ForwardingListenableFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.lang.buildfile.validation.GlobPatternValidator;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileSystem;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+
+/**
+ * Implementation of a subset of UNIX-style file globbing, expanding "*" and "?" as wildcards, but
+ * not [a-z] ranges.
+ *
+ * <p><code>**</code> gets special treatment in include patterns. If it is used as a complete path
+ * segment it matches the filenames in subdirectories recursively.
+ *
+ * Largely copied from {@link com.google.devtools.build.lib.vfs.UnixGlob}
+ */
+public final class UnixGlob {
+  private UnixGlob() {}
+
+  private static List<File> globInternal(File base,
+                                         Collection<String> patterns,
+                                         Collection<String> excludePatterns,
+                                         boolean excludeDirectories,
+                                         Predicate<File> dirPred,
+                                         ThreadPoolExecutor threadPool)
+    throws IOException, InterruptedException {
+
+    GlobVisitor visitor = (threadPool == null)
+                          ? new GlobVisitor()
+                          : new GlobVisitor(threadPool);
+    return visitor.glob(base, patterns, excludePatterns, excludeDirectories, dirPred);
+  }
+
+  private static Future<List<File>> globAsyncInternal(File base,
+                                                      Collection<String> patterns,
+                                                      Collection<String> excludePatterns,
+                                                      boolean excludeDirectories,
+                                                      Predicate<File> dirPred,
+                                                      ThreadPoolExecutor threadPool) {
+    Preconditions.checkNotNull(threadPool, "%s %s", base, patterns);
+    try {
+      return new GlobVisitor(threadPool)
+        .globAsync(base, patterns, excludePatterns, excludeDirectories, dirPred);
+    } catch (IOException e) {
+      // We are evaluating asynchronously, so no exceptions should be thrown until the future is
+      // retrieved.
+      throw new IllegalStateException(e);
+    }
+  }
+
+  /**
+   * Checks that each pattern is valid, splits it into segments and checks
+   * that each segment contains only valid wildcards.
+   *
+   * @return list of segment arrays
+   */
+  private static List<String[]> checkAndSplitPatterns(Collection<String> patterns) {
+    List<String[]> list = Lists.newArrayListWithCapacity(patterns.size());
+    for (String pattern : patterns) {
+      String error = GlobPatternValidator.validate(pattern);
+      if (error != null) {
+        throw new IllegalArgumentException(error);
+      }
+      Iterable<String> segments = Splitter.on('/').split(pattern);
+      list.add(Iterables.toArray(segments, String.class));
+    }
+    return list;
+  }
+
+  private static boolean excludedOnMatch(File path,
+                                         List<String[]> excludePatterns,
+                                         int idx,
+                                         Cache<String, Pattern> cache) {
+    for (String[] excludePattern : excludePatterns) {
+      String text = path.getName();
+      if (idx == excludePattern.length
+          && matches(excludePattern[idx - 1], text, cache)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns the exclude patterns in {@code excludePatterns} which could
+   * apply to the children of {@code base}
+   *
+   * @param idx index into {@code excludePatterns} for the part of the pattern
+   *        which might match {@code base}
+   */
+  private static List<String[]> getRelevantExcludes(
+    final File base,
+    List<String[]> excludePatterns,
+    final int idx,
+    final Cache<String, Pattern> cache) {
+
+    if (excludePatterns.isEmpty()) {
+      return excludePatterns;
+    }
+    List<String[]> list = new ArrayList<>();
+    for (String[] patterns : excludePatterns) {
+      if (excludePatternMatches(patterns, idx, base, cache)) {
+        list.add(patterns);
+      }
+    }
+    return list;
+  }
+
+  /**
+   * @param patterns a list of patterns
+   * @param idx index into {@code patterns}
+   */
+  private static boolean excludePatternMatches(String[] patterns,
+                                               int idx,
+                                               File base,
+                                               Cache<String, Pattern> cache) {
+    if (idx == 0) {
+      return true;
+    }
+    String text = base.getName();
+    return patterns.length > idx && matches(patterns[idx - 1], text, cache);
+  }
+
+  /**
+   * Calls {@link #matches(String, String, Cache) matches(pattern, str, null)}
+   */
+  public static boolean matches(String pattern, String str) {
+    return matches(pattern, str, null);
+  }
+
+  /**
+   * Returns whether {@code str} matches the glob pattern {@code pattern}. This
+   * method may use the {@code patternCache} to speed up the matching process.
+   *
+   * @param pattern a glob pattern
+   * @param str the string to match
+   * @param patternCache a cache from patterns to compiled Pattern objects, or
+   *        {@code null} to skip caching
+   */
+  public static boolean matches(String pattern,
+                                String str,
+                                Cache<String, Pattern> patternCache) {
+    if (pattern.length() == 0 || str.length() == 0) {
+      return false;
+    }
+
+    // Common case: **
+    if (pattern.equals("**")) {
+      return true;
+    }
+
+    // Common case: *
+    if (pattern.equals("*")) {
+      return true;
+    }
+
+    // If a filename starts with '.', this char must be matched explicitly.
+    if (str.charAt(0) == '.' && pattern.charAt(0) != '.') {
+      return false;
+    }
+
+    // Common case: *.xyz
+    if (pattern.charAt(0) == '*' && pattern.lastIndexOf('*') == 0) {
+      return str.endsWith(pattern.substring(1));
+    }
+    // Common case: xyz*
+    int lastIndex = pattern.length() - 1;
+    // The first clause of this if statement is unnecessary, but is an
+    // optimization--charAt runs faster than indexOf.
+    if (pattern.charAt(lastIndex) == '*' && pattern.indexOf('*') == lastIndex) {
+      return str.startsWith(pattern.substring(0, lastIndex));
+    }
+
+    Pattern regex = patternCache == null ? null : patternCache.getIfPresent(pattern);
+    if (regex == null) {
+      regex = makePatternFromWildcard(pattern);
+      if (patternCache != null) {
+        patternCache.put(pattern, regex);
+      }
+    }
+    return regex.matcher(str).matches();
+  }
+
+  /**
+   * Returns a regular expression implementing a matcher for "pattern", in which
+   * "*" and "?" are wildcards.
+   *
+   * <p>e.g. "foo*bar?.java" -> "foo.*bar.\\.java"
+   */
+  private static Pattern makePatternFromWildcard(String pattern) {
+    StringBuilder regexp = new StringBuilder();
+    for(int i = 0, len = pattern.length(); i < len; i++) {
+      char c = pattern.charAt(i);
+      switch(c) {
+        case '*':
+          int toIncrement = 0;
+          if (len > i + 1 && pattern.charAt(i + 1) == '*') {
+            // The pattern '**' is interpreted to match 0 or more directory separators, not 1 or
+            // more. We skip the next * and then find a trailing/leading '/' and get rid of it.
+            toIncrement = 1;
+            if (len > i + 2 && pattern.charAt(i + 2) == '/') {
+              // We have '**/' -- skip the '/'.
+              toIncrement = 2;
+            } else if (len == i + 2 && i > 0 && pattern.charAt(i - 1) == '/') {
+              // We have '/**' -- remove the '/'.
+              regexp.delete(regexp.length() - 1, regexp.length());
+            }
+          }
+          regexp.append(".*");
+          i += toIncrement;
+          break;
+        case '?':
+          regexp.append('.');
+          break;
+        //escape the regexp special characters that are allowed in wildcards
+        case '^': case '$': case '|': case '+':
+        case '{': case '}': case '[': case ']':
+        case '\\': case '.':
+          regexp.append('\\');
+          regexp.append(c);
+          break;
+        default:
+          regexp.append(c);
+          break;
+      }
+    }
+    return Pattern.compile(regexp.toString());
+  }
+
+  public static Builder forPath(File path) {
+    return new Builder(path);
+  }
+
+  /**
+   * Builder class for UnixGlob.
+   *
+   *
+   */
+  public static class Builder {
+    private File base;
+    private List<String> patterns;
+    private List<String> excludes;
+    private boolean excludeDirectories;
+    private Predicate<File> pathFilter;
+    private ThreadPoolExecutor threadPool;
+
+    /**
+     * Creates a glob builder with the given base path.
+     */
+    public Builder(File base) {
+      this.base = base;
+      this.patterns = Lists.newArrayList();
+      this.excludes = Lists.newArrayList();
+      this.excludeDirectories = false;
+      this.pathFilter = file -> true;
+    }
+
+    /**
+     * Adds a pattern to include to the glob builder.
+     *
+     * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+     */
+    public Builder addPattern(String pattern) {
+      this.patterns.add(pattern);
+      return this;
+    }
+
+    /**
+     * Adds a pattern to include to the glob builder.
+     *
+     * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+     */
+    public Builder addPatterns(String... patterns) {
+      Collections.addAll(this.patterns, patterns);
+      return this;
+    }
+
+    /**
+     * Adds a pattern to include to the glob builder.
+     *
+     * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+     */
+    public Builder addPatterns(Collection<String> patterns) {
+      this.patterns.addAll(patterns);
+      return this;
+    }
+
+    /**
+     * Adds patterns to exclude from the results to the glob builder.
+     *
+     * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+     */
+    public Builder addExcludes(String... excludes) {
+      Collections.addAll(this.excludes, excludes);
+      return this;
+    }
+
+    /**
+     * Adds patterns to exclude from the results to the glob builder.
+     *
+     * <p>For a description of the syntax of the patterns, see {@link UnixGlob}.
+     */
+    public Builder addExcludes(Collection<String> excludes) {
+      this.excludes.addAll(excludes);
+      return this;
+    }
+
+    /**
+     * If set to true, directories are not returned in the glob result.
+     */
+    public Builder setExcludeDirectories(boolean excludeDirectories) {
+      this.excludeDirectories = excludeDirectories;
+      return this;
+    }
+
+
+    /**
+     * Sets the threadpool to use for parallel glob evaluation.
+     * If unset, evaluation is done in-thread.
+     */
+    public Builder setThreadPool(ThreadPoolExecutor pool) {
+      this.threadPool = pool;
+      return this;
+    }
+
+
+    /**
+     * If set, the given predicate is called for every directory
+     * encountered. If it returns false, the corresponding item is not
+     * returned in the output and directories are not traversed either.
+     */
+    public Builder setDirectoryFilter(Predicate<File> pathFilter) {
+      this.pathFilter = pathFilter;
+      return this;
+    }
+
+    /**
+     * Executes the glob.
+     *
+     * @throws InterruptedException if the thread is interrupted.
+     */
+    public List<File> glob() throws IOException, InterruptedException {
+      return globInternal(base, patterns, excludes, excludeDirectories, pathFilter, threadPool);
+    }
+
+    /**
+     * Executes the glob asynchronously. {@link #setThreadPool} must have been called already with a
+     * non-null argument.
+     *
+     * @param checkForInterrupt if the returned future may throw InterruptedException.
+     */
+    public Future<List<File>> globAsync() {
+      return globAsyncInternal(base, patterns, excludes, excludeDirectories, pathFilter, threadPool);
+    }
+  }
+
+  /**
+   * Adapts the result of the glob visitation as a Future.
+   */
+  private static class GlobFuture extends ForwardingListenableFuture<List<File>> {
+    private final GlobVisitor visitor;
+    private final SettableFuture<List<File>> delegate = SettableFuture.create();
+
+    public GlobFuture(GlobVisitor visitor) {
+      this.visitor = visitor;
+    }
+
+    @Override
+    public List<File> get() throws InterruptedException, ExecutionException {
+      return super.get();
+    }
+
+    @Override
+    protected ListenableFuture<List<File>> delegate() {
+      return delegate;
+    }
+
+    public void setException(IOException exception) {
+      delegate.setException(exception);
+    }
+
+    public void set(List<File> paths) {
+      delegate.set(paths);
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+      // Best-effort interrupt of the in-flight visitation.
+      visitor.cancel();
+      return true;
+    }
+
+    public void markCanceled() {
+      super.cancel(true);
+    }
+  }
+
+  /**
+   * GlobVisitor executes a glob using parallelism, which is useful when
+   * the glob() requires many readdir() calls on high latency filesystems.
+   */
+  private static final class GlobVisitor {
+    // These collections are used across workers and must therefore be thread-safe.
+    private final Collection<File> results = Sets.newConcurrentHashSet();
+    private final Cache<String, Pattern> cache = CacheBuilder.newBuilder().build(
+      new CacheLoader<String, Pattern>() {
+        @Override
+        public Pattern load(String wildcard) {
+          return makePatternFromWildcard(wildcard);
+        }
+      });
+
+    private final GlobFuture result;
+    private final ThreadPoolExecutor executor;
+    private final AtomicLong pendingOps = new AtomicLong(0);
+    private final AtomicReference<IOException> failure = new AtomicReference<>();
+    private final FileAttributeProvider fileAttributeProvider = FileAttributeProvider.getInstance();
+    private volatile boolean canceled = false;
+
+    public GlobVisitor(ThreadPoolExecutor executor) {
+      this.executor = executor;
+      this.result = new GlobFuture(this);
+    }
+
+    public GlobVisitor() {
+      this(null);
+    }
+
+    /**
+     * Performs wildcard globbing: returns the sorted list of filenames that match any of
+     * {@code patterns} relative to {@code base}, but which do not match {@code excludePatterns}.
+     * Directories are traversed if and only if they match {@code dirPred}. The predicate is also
+     * called for the root of the traversal.
+     *
+     * <p>Patterns may include "*" and "?", but not "[a-z]".
+     *
+     * <p><code>**</code> gets special treatment in include patterns. If it is
+     * used as a complete path segment it matches the filenames in
+     * subdirectories recursively.
+     *
+     * @throws IllegalArgumentException if any glob or exclude pattern
+     *         {@linkplain #checkPatternForError(String) contains errors} or if
+     *         any exclude pattern segment contains <code>**</code> or if any
+     *         include pattern segment contains <code>**</code> but not equal to
+     *         it.
+     */
+    public List<File> glob(File base,
+                           Collection<String> patterns,
+                           Collection<String> excludePatterns,
+                           boolean excludeDirectories,
+                           Predicate<File> dirPred) throws IOException, InterruptedException {
+      try {
+        return globAsync(base, patterns, excludePatterns, excludeDirectories, dirPred).get();
+      } catch (ExecutionException e) {
+        Throwable cause = e.getCause();
+        Throwables.propagateIfPossible(cause, IOException.class);
+        throw new RuntimeException(e);
+      }
+    }
+
+    public Future<List<File>> globAsync(File base, Collection<String> patterns,
+                                        Collection<String> excludePatterns, boolean excludeDirectories,
+                                        Predicate<File> dirPred) throws IOException {
+
+      if (!fileAttributeProvider.exists(base) || patterns.isEmpty()) {
+        return Futures.immediateFuture(Collections.emptyList());
+      }
+      boolean baseIsDirectory = fileAttributeProvider.isDirectory(base);
+
+      List<String[]> splitPatterns = checkAndSplitPatterns(patterns);
+      List<String[]> splitExcludes = checkAndSplitPatterns(excludePatterns);
+
+      // We do a dumb loop, even though it will likely duplicate work
+      // (e.g., readdir calls). In order to optimize, we would need
+      // to keep track of which patterns shared sub-patterns and which did not
+      // (for example consider the glob [*/*.java, sub/*.java, */*.txt]).
+      pendingOps.incrementAndGet();
+      try {
+        for (String[] splitPattern : splitPatterns) {
+          queueGlob(base, baseIsDirectory, splitPattern, 0, excludeDirectories, splitExcludes, 0, results, cache, dirPred);
+        }
+      } finally {
+        decrementAndCheckDone();
+      }
+
+      return result;
+    }
+
+    private void queueGlob(File base,
+                           boolean baseIsDirectory,
+                           String[] patternParts,
+                           int idx,
+                           boolean excludeDirectories,
+                           List<String[]> excludePatterns,
+                           int excludeIdx,
+                           Collection<File> results,
+                           Cache<String, Pattern> cache,
+                           Predicate<File> dirPred) throws IOException {
+      enqueue(() -> {
+        try {
+          reallyGlob(base, baseIsDirectory, patternParts, idx, excludeDirectories,
+                     excludePatterns, excludeIdx, results, cache, dirPred);
+        } catch (IOException e) {
+          failure.set(e);
+        }
+      });
+    }
+
+    protected void enqueue(final Runnable r) {
+      pendingOps.incrementAndGet();
+
+      Runnable wrapped = () -> {
+        try {
+          if (!canceled && failure.get() == null) {
+            r.run();
+          }
+        } finally {
+          decrementAndCheckDone();
+        }
+      };
+
+      if (executor == null) {
+        wrapped.run();
+      } else {
+        executor.execute(wrapped);
+      }
+    }
+
+    protected void cancel() {
+      this.canceled = true;
+    }
+
+    private void decrementAndCheckDone() {
+      if (pendingOps.decrementAndGet() == 0) {
+        // We get to 0 iff we are done all the relevant work. This is because we always increment
+        // the pending ops count as we're enqueuing, and don't decrement until the task is complete
+        // (which includes accounting for any additional tasks that one enqueues).
+        if (canceled) {
+          result.markCanceled();
+        } else if (failure.get() != null) {
+          result.setException(failure.get());
+        } else {
+          result.set(Ordering.<File>natural().immutableSortedCopy(results));
+        }
+      }
+    }
+
+    /**
+     * Expressed in Haskell:
+     * <pre>
+     *  reallyGlob base []     = { base }
+     *  reallyGlob base [x:xs] = union { reallyGlob(f, xs) | f results "base/x" }
+     * </pre>
+     */
+    private void reallyGlob(File base,
+                            boolean baseIsDirectory,
+                            String[] patternParts,
+                            int idx,
+                            boolean excludeDirectories,
+                            List<String[]> excludePatterns,
+                            int excludeIdx,
+                            Collection<File> results,
+                            Cache<String, Pattern> cache,
+                            Predicate<File> dirPred) throws IOException {
+      ProgressManager.checkCanceled();
+      if (baseIsDirectory && !dirPred.test(base)) {
+        return;
+      }
+
+      if (idx == patternParts.length) { // Base case.
+        if (!(excludeDirectories && baseIsDirectory) &&
+            !excludedOnMatch(base, excludePatterns, excludeIdx, cache)) {
+          results.add(base);
+        }
+        return;
+      }
+
+      if (!baseIsDirectory) {
+        // Nothing to find here.
+        return;
+      }
+
+      List<String[]> relevantExcludes = getRelevantExcludes(base, excludePatterns, excludeIdx, cache);
+      final String pattern = patternParts[idx];
+
+      // ** is special: it can match nothing at all.
+      // For example, x/** matches x, **/y matches y, and x/**/y matches x/y.
+      if ("**".equals(pattern)) {
+        queueGlob(base, baseIsDirectory, patternParts, idx + 1, excludeDirectories, excludePatterns, excludeIdx, results, cache, dirPred);
+      }
+
+      if (!pattern.contains("*") && !pattern.contains("?")) {
+        // We do not need to do a readdir in this case, just a stat.
+        File child = new File(base, pattern);
+        boolean childIsDir = fileAttributeProvider.isDirectory(child);
+        if (!childIsDir && !fileAttributeProvider.isFile(child)) {
+          // The file is a dangling symlink, fifo, does not exist, etc.
+          return;
+        }
+
+        queueGlob(child, childIsDir, patternParts, idx + 1, excludeDirectories,
+                  relevantExcludes, excludeIdx + 1, results, cache, dirPred);
+        return;
+      }
+
+      File[] children = getChildren(base);
+      if (children == null) {
+        return;
+      }
+      for (File child : children) {
+        boolean childIsDir = fileAttributeProvider.isDirectory(child);
+
+        if ("**".equals(pattern)) {
+          // Recurse without shifting the pattern.
+          if (childIsDir) {
+            queueGlob(child, childIsDir, patternParts, idx, excludeDirectories,
+                      relevantExcludes, excludeIdx + 1, results, cache, dirPred);
+          }
+        }
+        if (matches(pattern, child.getName(), cache)) {
+          // Recurse and consume one segment of the pattern.
+          if (childIsDir) {
+            queueGlob(child, childIsDir, patternParts, idx + 1, excludeDirectories,
+                      relevantExcludes, excludeIdx + 1, results, cache, dirPred);
+          } else {
+            // Instead of using an async call, just repeat the base case above.
+            if (idx + 1 == patternParts.length &&
+                !excludedOnMatch(child, relevantExcludes, excludeIdx + 1, cache)) {
+              results.add(child);
+            }
+          }
+        }
+      }
+    }
+
+    private static VirtualFileSystem getFileSystem() {
+      if (ApplicationManager.getApplication().isUnitTestMode()) {
+        return TempFileSystem.getInstance();
+      }
+      return LocalFileSystem.getInstance();
+    }
+
+    @Nullable
+    private File[] getChildren(File file) {
+      if (!ApplicationManager.getApplication().isUnitTestMode()) {
+        return file.listFiles();
+      }
+      TempFileSystem fs = TempFileSystem.getInstance();
+      VirtualFile vf = fs.findFileByIoFile(file);
+      VirtualFile[] children = vf.getChildren();
+      if (children == null) {
+        return null;
+      }
+      return Arrays.stream(children)
+        .map(VirtualFile::getPath)
+        .map(File::new)
+        .toArray(File[]::new);
+    }
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildColorsPage.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildColorsPage.java
new file mode 100644
index 0000000..79d1d2c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildColorsPage.java
@@ -0,0 +1,112 @@
+/*
+ * 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.highlighting;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.application.options.colors.InspectionColorSettingsPage;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
+import com.intellij.openapi.options.colors.AttributesDescriptor;
+import com.intellij.openapi.options.colors.ColorDescriptor;
+import com.intellij.openapi.options.colors.ColorSettingsPage;
+import com.intellij.psi.codeStyle.DisplayPriority;
+import com.intellij.psi.codeStyle.DisplayPrioritySortable;
+import com.intellij.util.PlatformUtils;
+
+import javax.swing.*;
+import java.util.Map;
+
+/**
+ * Allows user to customize colors.
+ */
+public class BuildColorsPage implements ColorSettingsPage, InspectionColorSettingsPage, DisplayPrioritySortable {
+  private static final AttributesDescriptor[] ATTRS = new AttributesDescriptor[] {
+    new AttributesDescriptor("Keyword", BuildSyntaxHighlighter.BUILD_KEYWORD),
+    new AttributesDescriptor("String", BuildSyntaxHighlighter.BUILD_STRING),
+    new AttributesDescriptor("Number", BuildSyntaxHighlighter.BUILD_NUMBER),
+    new AttributesDescriptor("Line Comment", BuildSyntaxHighlighter.BUILD_LINE_COMMENT),
+    new AttributesDescriptor("Operation Sign", BuildSyntaxHighlighter.BUILD_OPERATION_SIGN),
+    new AttributesDescriptor("Parentheses", BuildSyntaxHighlighter.BUILD_PARENS),
+    new AttributesDescriptor("Brackets", BuildSyntaxHighlighter.BUILD_BRACKETS),
+    new AttributesDescriptor("Braces", BuildSyntaxHighlighter.BUILD_BRACES),
+    new AttributesDescriptor("Comma", BuildSyntaxHighlighter.BUILD_COMMA),
+    new AttributesDescriptor("Dot", BuildSyntaxHighlighter.BUILD_DOT),
+    new AttributesDescriptor("Function definition", BuildSyntaxHighlighter.BUILD_FN_DEFINITION),
+    new AttributesDescriptor("Parameter", BuildSyntaxHighlighter.BUILD_PARAMETER),
+    new AttributesDescriptor("Keyword argument", BuildSyntaxHighlighter.BUILD_KEYWORD_ARG),
+  };
+
+  private static final Map<String,TextAttributesKey> ourTagToDescriptorMap = ImmutableMap.<String, TextAttributesKey>builder()
+    .put("funcDef", BuildSyntaxHighlighter.BUILD_FN_DEFINITION)
+    .put("param", BuildSyntaxHighlighter.BUILD_PARAMETER)
+    .put("kwarg", BuildSyntaxHighlighter.BUILD_KEYWORD_ARG)
+    .put("comma", BuildSyntaxHighlighter.BUILD_COMMA)
+    .put("number", BuildSyntaxHighlighter.BUILD_NUMBER)
+    .build();
+
+  @Override
+  public String getDisplayName() {
+    return Blaze.defaultBuildSystemName() + " BUILD files";
+  }
+
+  @Override
+  public Icon getIcon() {
+    return BuildFileType.INSTANCE.getIcon();
+  }
+
+  @Override
+  public AttributesDescriptor[] getAttributeDescriptors() {
+    return ATTRS;
+  }
+
+  @Override
+  public ColorDescriptor[] getColorDescriptors() {
+    return ColorDescriptor.EMPTY_ARRAY;
+  }
+
+  @Override
+  public SyntaxHighlighter getHighlighter() {
+    final SyntaxHighlighter highlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(BuildFileType.INSTANCE, null, null);
+    assert highlighter != null;
+    return highlighter;
+  }
+
+  @Override
+  public String getDemoText() {
+    return
+      "def <funcDef>function</funcDef>(<param>x</param>, <kwarg>whatever</kwarg>=1):\n" +
+        "    s = (\"Test\", 2+3, {'a': 'b'}, <param>x</param>)   # Comment\n" +
+        "    print s[0].lower()\n"+
+        "\n"+
+        "java_library(\n" +
+        "    <kwarg>name</kwarg> = \"lib\",\n" +
+        "    <kwarg>srcs</kwarg> = glob([\"**/*.java\"]),\n" +
+        ")\n";
+  }
+
+  @Override
+  public Map<String, TextAttributesKey> getAdditionalHighlightingTagToDescriptorMap() {
+    return ourTagToDescriptorMap;
+  }
+
+  @Override
+  public DisplayPriority getPriority() {
+    return PlatformUtils.isPyCharm() ? DisplayPriority.KEY_LANGUAGE_SETTINGS : DisplayPriority.LANGUAGE_SETTINGS;
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildSyntaxHighlighter.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildSyntaxHighlighter.java
new file mode 100644
index 0000000..537027c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildSyntaxHighlighter.java
@@ -0,0 +1,97 @@
+/*
+ * 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.highlighting;
+
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexer;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexerBase;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterBase;
+import com.intellij.psi.tree.IElementType;
+
+import java.util.Map;
+
+import static com.intellij.openapi.editor.DefaultLanguageHighlighterColors.*;
+
+/**
+ * This class maps tokens to highlighting attributes. Each attribute contains the font properties.
+ */
+public class BuildSyntaxHighlighter extends SyntaxHighlighterBase {
+
+  public static final TextAttributesKey BUILD_KEYWORD = key("BUILD.MODIFIER", KEYWORD);
+  public static final TextAttributesKey BUILD_STRING = key("BUILD.STRING", STRING);
+  public static final TextAttributesKey BUILD_NUMBER = key("BUILD.NUMBER", NUMBER);
+  public static final TextAttributesKey BUILD_LINE_COMMENT = key("BUILD.LINE_COMMENT", LINE_COMMENT);
+  public static final TextAttributesKey BUILD_BRACES = key("BUILD.BRACES", BRACES);
+  public static final TextAttributesKey BUILD_PARENS = key("BUILD.PARENS", PARENTHESES);
+  public static final TextAttributesKey BUILD_BRACKETS = key("BUILD.BRACKETS", BRACKETS);
+  public static final TextAttributesKey BUILD_OPERATION_SIGN = key("BUILD.OPERATION_SIGN", OPERATION_SIGN);
+  public static final TextAttributesKey BUILD_DOT = key("BUILD.DOT", DOT);
+  public static final TextAttributesKey BUILD_SEMICOLON = key("BUILD.SEMICOLON", SEMICOLON);
+  public static final TextAttributesKey BUILD_COMMA = key("BUILD.COMMA", COMMA);
+  public static final TextAttributesKey BUILD_PARAMETER = key("BUILD.PARAMETER", PARAMETER);
+  public static final TextAttributesKey BUILD_KEYWORD_ARG = key("BUILD.KEYWORD.ARG", PARAMETER);
+  public static final TextAttributesKey BUILD_FN_DEFINITION = key("BUILD.FN.DEFINITION", FUNCTION_DECLARATION);
+
+  private static TextAttributesKey key(String name, TextAttributesKey fallbackKey) {
+    return TextAttributesKey.createTextAttributesKey(name, fallbackKey);
+  }
+
+  private static final Map<IElementType, TextAttributesKey> keys = Maps.newHashMap();
+
+  static {
+    addAttribute(TokenKind.COMMENT, BUILD_LINE_COMMENT);
+    addAttribute(TokenKind.INT, BUILD_NUMBER);
+    addAttribute(TokenKind.STRING, BUILD_STRING);
+
+    addAttribute(TokenKind.LBRACE, BUILD_BRACES);
+    addAttribute(TokenKind.RBRACE, BUILD_BRACES);
+    addAttribute(TokenKind.LBRACKET, BUILD_BRACKETS);
+    addAttribute(TokenKind.RBRACKET, BUILD_BRACKETS);
+
+    addAttribute(TokenKind.LPAREN, BUILD_PARENS);
+    addAttribute(TokenKind.RPAREN, BUILD_PARENS);
+
+    addAttribute(TokenKind.DOT, BUILD_DOT);
+    addAttribute(TokenKind.SEMI, BUILD_SEMICOLON);
+    addAttribute(TokenKind.COMMA, BUILD_COMMA);
+
+    for (TokenKind kind : TokenKind.OPERATIONS) {
+      addAttribute(kind, BUILD_OPERATION_SIGN);
+    }
+
+    for (TokenKind kind : TokenKind.KEYWORDS) {
+      addAttribute(kind, BUILD_KEYWORD);
+    }
+  }
+
+  private static void addAttribute(TokenKind kind, TextAttributesKey key) {
+    keys.put(BuildToken.fromKind(kind), key);
+  }
+
+  @Override
+  public Lexer getHighlightingLexer() {
+    return new BuildLexer(BuildLexerBase.LexerMode.SyntaxHighlighting);
+  }
+
+  @Override
+  public TextAttributesKey[] getTokenHighlights(IElementType iElementType) {
+    return pack(keys.get(iElementType));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildSyntaxHighlighterFactory.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildSyntaxHighlighterFactory.java
new file mode 100644
index 0000000..a1ef7d5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/highlighting/BuildSyntaxHighlighterFactory.java
@@ -0,0 +1,32 @@
+/*
+ * 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.highlighting;
+
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+
+/**
+ * Factory for BuildSyntaxHighlighter
+ */
+public class BuildSyntaxHighlighterFactory extends SyntaxHighlighterFactory {
+
+  @Override
+  public SyntaxHighlighter getSyntaxHighlighter(Project project, VirtualFile virtualFile) {
+    return new BuildSyntaxHighlighter();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileLanguage.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileLanguage.java
new file mode 100644
index 0000000..46c0b85
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileLanguage.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.lang.buildfile.language;
+
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.intellij.lang.Language;
+
+/**
+ * BUILD file language
+ */
+public class BuildFileLanguage extends Language {
+
+  public static final BoolExperiment BUILD_FILE_SUPPORT_ENABLED = new BoolExperiment("build.file.support.enabled", true);
+
+  public static boolean buildFileSupportEnabled() {
+    return BUILD_FILE_SUPPORT_ENABLED.getValue() && BlazeUserSettings.getInstance().getBuildFileSupportEnabled();
+  }
+
+  public static final BuildFileLanguage INSTANCE = new BuildFileLanguage();
+
+  private BuildFileLanguage() {
+    super("BUILD", "text/python");
+  }
+
+  @Override
+  public String getDisplayName() {
+    return "BUILD file";
+  }
+
+  @Override
+  public boolean isCaseSensitive() {
+    return true;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileType.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileType.java
new file mode 100644
index 0000000..c09c8b8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileType.java
@@ -0,0 +1,58 @@
+/*
+ * 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.language;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import icons.BlazeIcons;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * BUILD file type
+ */
+public class BuildFileType extends LanguageFileType {
+
+  public static final BuildFileType INSTANCE = new BuildFileType();
+
+  private BuildFileType() {
+    super(BuildFileLanguage.INSTANCE);
+  }
+
+  @Override
+  public String getName() {
+    // Warning: this is conflated with Language.myID in several places...
+    // They must be identical.
+    return BuildFileLanguage.INSTANCE.getID();
+  }
+
+  @Override
+  public String getDescription() {
+    return Blaze.defaultBuildSystemName() + " BUILD language";
+  }
+
+  @Override
+  public String getDefaultExtension() {
+    return "";
+  }
+
+  @Override
+  @Nullable
+  public Icon getIcon() {
+    return BlazeIcons.BuildFile;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.java
new file mode 100644
index 0000000..864a036
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeFactory.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.lang.buildfile.language;
+
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.fileTypes.*;
+import com.intellij.openapi.fileTypes.ex.FileTypeManagerEx;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Factory for BuildFileType
+ */
+public class BuildFileTypeFactory extends FileTypeFactory {
+
+  private static ImmutableList<FileNameMatcher> DEFAULT_ASSOCIATIONS = ImmutableList.of(
+    new ExactFileNameMatcher("BUILD"),
+    new ExtensionFileNameMatcher("bzl")
+  );
+
+  @Override
+  public void createFileTypes(@NotNull final FileTypeConsumer consumer) {
+    consumer.consume(BuildFileType.INSTANCE, DEFAULT_ASSOCIATIONS.toArray(new FileNameMatcher[0]));
+  }
+
+  private static volatile boolean enabled = true;
+
+  public static void updateBuildFileLanguageEnabled(boolean supportEnabled) {
+    if (enabled == supportEnabled) {
+      return;
+    }
+    enabled = supportEnabled;
+    if (!supportEnabled) {
+      FileTypeManagerEx.getInstanceEx().unregisterFileType(BuildFileType.INSTANCE);
+    } else {
+      FileTypeManager.getInstance().registerFileType(BuildFileType.INSTANCE, DEFAULT_ASSOCIATIONS);
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/AttributeDefinition.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/AttributeDefinition.java
new file mode 100644
index 0000000..3907a58
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/AttributeDefinition.java
@@ -0,0 +1,78 @@
+/*
+ * 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.language.semantics;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.repackaged.devtools.build.lib.query2.proto.proto2api.Build;
+
+import javax.annotation.Nullable;
+import java.io.Serializable;
+
+/**
+ * Simple implementation of AttributeDefinition, from build.proto
+ */
+public class AttributeDefinition implements Serializable {
+
+  public static AttributeDefinition fromProto(Build.AttributeDefinition attr) {
+    return new AttributeDefinition(
+      attr.getName(),
+      attr.getType(),
+      attr.getMandatory(),
+      attr.hasDocumentation() ? attr.getDocumentation() : null,
+      getAllowedRuleClasses(attr)
+    );
+  }
+
+  @Nullable
+  private static ImmutableList<String> getAllowedRuleClasses(Build.AttributeDefinition attr) {
+    if (!attr.hasAllowedRuleClasses()) {
+      return null;
+    }
+    return ImmutableList.copyOf(attr.getAllowedRuleClasses().getAllowedRuleClassList());
+  }
+
+  public final String name;
+  public final Build.Attribute.Discriminator type;
+  public final boolean mandatory;
+  public final String documentation;
+
+  // the names of rules allowed in this attribute, or null if any rules are allowed.
+  @Nullable private final ImmutableList<String> allowedRuleClasses;
+
+  @VisibleForTesting
+  public AttributeDefinition(
+    String name,
+    Build.Attribute.Discriminator type,
+    boolean mandatory,
+    String documentation,
+    @Nullable ImmutableList<String> allowedRuleClasses) {
+
+    this.name = name;
+    this.type = type;
+    this.mandatory = mandatory;
+    this.documentation = documentation;
+    this.allowedRuleClasses = allowedRuleClasses;
+  }
+
+  /**
+   * Only relevant for attributes of type LABEL and LABEL_LIST.
+   * Some such attributes can only contain certain rule types.
+   */
+  public boolean isRuleTypeAllowed(RuleDefinition rule) {
+    return allowedRuleClasses == null || allowedRuleClasses.contains(rule.name);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpec.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpec.java
new file mode 100644
index 0000000..7c2b14e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpec.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.language.semantics;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.repackaged.devtools.build.lib.query2.proto.proto2api.Build;
+
+import javax.annotation.Nullable;
+import java.io.Serializable;
+
+/**
+ * Specification of the BUILD language, as provided by "blaze info build-language".<p>
+ *
+ * This constitutes a set of rules, along with their supported attributes, and other
+ * useful information. We query this once per blaze workspace (it won't change unless
+ * the blaze binary is also changed).<p>
+ *
+ * This rule list is not exhaustive; it's intended to give information about known
+ * rules, not enumerate all possibilities.
+ */
+public class BuildLanguageSpec implements Serializable {
+
+  public static BuildLanguageSpec fromProto(Build.BuildLanguage proto) {
+    ImmutableMap.Builder<String, RuleDefinition> builder = ImmutableMap.builder();
+    for (Build.RuleDefinition rule : proto.getRuleList()) {
+      builder.put(rule.getName(), RuleDefinition.fromProto(rule));
+    }
+    return new BuildLanguageSpec(builder.build());
+  }
+
+  public final ImmutableMap<String, RuleDefinition> rules;
+
+  @VisibleForTesting
+  public BuildLanguageSpec(ImmutableMap<String, RuleDefinition> rules) {
+    this.rules = rules;
+  }
+
+  public ImmutableSet<String> getKnownRuleNames() {
+    return rules.keySet();
+  }
+
+  public boolean hasRule(@Nullable String ruleName) {
+    return getRule(ruleName) != null;
+  }
+
+  @Nullable
+  public RuleDefinition getRule(@Nullable String ruleName) {
+    return ruleName != null ? rules.get(ruleName) : null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProvider.java
new file mode 100644
index 0000000..984c286
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProvider.java
@@ -0,0 +1,39 @@
+/*
+ * 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.language.semantics;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides a BuildLanguageSpec for the given project.
+ */
+public interface BuildLanguageSpecProvider {
+
+  static BuildLanguageSpecProvider getInstance() {
+    return ServiceManager.getService(BuildLanguageSpecProvider.class);
+  }
+
+  /**
+   * Returns null if cancelled or unsuccessful. Also returns null if no WorkspaceRoot can be found for the
+   * project (i.e. because BlazeImportSettings has not been set).
+   */
+  @Nullable
+  BuildLanguageSpec getLanguageSpec(Project project);
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.java
new file mode 100644
index 0000000..eae6d8c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/BuildLanguageSpecProviderImpl.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.lang.buildfile.language.semantics;
+
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.lang.buildfile.sync.LanguageSpecResult;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.intellij.openapi.project.Project;
+
+import java.util.Map;
+
+/**
+ * Calls 'blaze info build-language', to retrieve the language spec.
+ */
+public class BuildLanguageSpecProviderImpl extends SyncListener.Adapter implements BuildLanguageSpecProvider {
+
+  private static final Map<Project, LanguageSpecResult> calculatedSpecs = Maps.newHashMap();
+
+  @Override
+  public BuildLanguageSpec getLanguageSpec(Project project) {
+    LanguageSpecResult result = calculatedSpecs.get(project);
+    return result != null ? result.spec : null;
+  }
+
+  @Override
+  public void onSyncComplete(Project project,
+                             BlazeImportSettings importSettings,
+                             ProjectViewSet projectViewSet,
+                             BlazeProjectData blazeProjectData) {
+    LanguageSpecResult spec = blazeProjectData.syncState.get(LanguageSpecResult.class);
+    if (spec != null) {
+      calculatedSpecs.put(project, spec);
+    }
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/RuleDefinition.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/RuleDefinition.java
new file mode 100644
index 0000000..6f93ea1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/language/semantics/RuleDefinition.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.language.semantics;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.repackaged.devtools.build.lib.query2.proto.proto2api.Build;
+
+import javax.annotation.Nullable;
+import java.io.Serializable;
+
+/**
+ * Simple implementation of RuleDefinition, from build.proto
+ */
+public class RuleDefinition implements Serializable {
+
+  /**
+   * This isn't included in the proto -- all other documented attributes seem to be.
+   */
+  private static final AttributeDefinition NAME_ATTRIBUTE = new AttributeDefinition(
+    "name", Build.Attribute.Discriminator.STRING, true, null, null);
+
+  public static RuleDefinition fromProto(Build.RuleDefinition rule) {
+    ImmutableMap.Builder<String, AttributeDefinition> map = ImmutableMap.builder();
+    for (Build.AttributeDefinition attr : rule.getAttributeList()) {
+      map.put(attr.getName(), AttributeDefinition.fromProto(attr));
+    }
+    map.put(NAME_ATTRIBUTE.name, NAME_ATTRIBUTE);
+    return new RuleDefinition(
+      rule.getName(),
+      map.build(),
+      rule.hasDocumentation() ? rule.getDocumentation() : null);
+  }
+
+  public final String name;
+  /**
+   * This map is not exhaustive; it only contains documented attributes.
+   */
+  public final ImmutableMap<String, AttributeDefinition> attributes;
+  @Nullable public final String documentation;
+
+  public RuleDefinition(String name, ImmutableMap<String, AttributeDefinition> attributes, @Nullable String documentation) {
+    this.name = name;
+    this.attributes = attributes;
+    this.documentation = documentation;
+  }
+
+  public ImmutableSet<String> getKnownAttributeNames() {
+    return attributes.keySet();
+  }
+
+  @Nullable
+  public AttributeDefinition getAttribute(@Nullable String attributeName) {
+    return attributeName != null ? attributes.get(attributeName) : null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildLexer.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildLexer.java
new file mode 100644
index 0000000..2ac477c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildLexer.java
@@ -0,0 +1,131 @@
+/*
+ * 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.lexer;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexerBase.LexerMode;
+import com.intellij.lexer.LexerBase;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Implementation of LexerBase using BuildLexerBase to tokenize the input.
+ */
+public class BuildLexer extends LexerBase {
+
+  private final LexerMode mode;
+
+  private int offsetEnd;
+  private int offsetStart;
+  private CharSequence buffer;
+  private Iterator<Token> tokens;
+  private Token currentToken;
+  private int currentState;
+
+  public BuildLexer(LexerMode mode) {
+    this.mode = mode;
+  }
+
+  @Override
+  public void start(CharSequence charSequence, int startOffset, int endOffset, int initialState) {
+    buffer = charSequence;
+    this.offsetEnd = endOffset;
+    this.offsetStart = startOffset;
+
+    BuildLexerBase lexer = new BuildLexerBase(charSequence.subSequence(startOffset, endOffset), initialState, mode);
+    checkNoCharactersMissing(charSequence.subSequence(startOffset, endOffset).length(), lexer.getTokens());
+    tokens = lexer.getTokens().iterator();
+    currentToken = null;
+    if (tokens.hasNext()) {
+      currentToken = tokens.next();
+    }
+    currentState = lexer.getOpenParenStackDepth();
+  }
+
+  /**
+   * Temporary debugging code. We need to tokenize every character in the input string.
+   */
+  private void checkNoCharactersMissing(int totalLength, List<Token> tokens) {
+    if (!tokens.isEmpty() && tokens.get(tokens.size() - 1).right != totalLength) {
+      String error = String.format("Lengths don't match: %s instead of %s",
+        tokens.get(tokens.size() - 1).right,
+        totalLength);
+      throw new RuntimeException(error);
+    }
+    int start = 0;
+    for (int i = 0; i < tokens.size(); i++) {
+      Token token = tokens.get(i);
+      if (token.left != start) {
+        throw new RuntimeException("Gap/inconsistency at: " + start);
+      }
+      start = token.right;
+    }
+  }
+
+
+  @Override
+  public int getState() {
+    return currentState;
+  }
+
+  @Override
+  public BuildToken getTokenType() {
+    if (currentToken != null) {
+      return BuildToken.fromKind(currentToken.kind);
+    }
+    return null;
+  }
+
+  @Override
+  public int getTokenStart() {
+    if (currentToken == null) {
+      return 0;
+    }
+    return currentToken.left + offsetStart;
+  }
+
+  @Override
+  public int getTokenEnd() {
+    if (currentToken == null) {
+      return 0;
+    }
+    return currentToken.right + offsetStart;
+  }
+
+  @Override
+  public void advance() {
+    if (tokens.hasNext()) {
+      currentToken = tokens.next();
+    } else {
+      currentToken = null;
+    }
+  }
+
+  public TokenKind getTokenKind() {
+    return currentToken.kind;
+  }
+
+  @Override
+  public CharSequence getBufferSequence() {
+    return buffer;
+  }
+
+  @Override
+  public int getBufferEnd() {
+    return offsetEnd;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildLexerBase.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildLexerBase.java
new file mode 100644
index 0000000..a71c884
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildLexerBase.java
@@ -0,0 +1,755 @@
+/*
+ * 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.lexer;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+/**
+ * A tokenizer for the BUILD language.
+ *
+ * Copied from blaze/bazel's lexer. The differences are:
+ * 1. Blaze's lexer isn't 'faithful', in that it reorders characters, skips characters, and adds
+ * ghost characters. We can't do that, because we need to match the editor's view of the document.
+ * 2. Blaze's lexer only lexes entire files (it can't incrementally lex part of a file, starting
+ * from a given indent stack depth).
+ */
+public class BuildLexerBase {
+
+  /**
+   * When tokenizing for the purposes of parsing, we handle indentation.<br>
+   * For syntax highlighting, we need to tokenize every character, and don't care about indentation.
+   */
+  public enum LexerMode {
+    Parsing,
+    SyntaxHighlighting
+  }
+
+  // Characters that can come immediately prior to an '=' character to generate
+  // a different token
+  private static final Map<Character, TokenKind> EQUAL_TOKENS =
+    ImmutableMap.<Character, TokenKind>builder()
+      .put('=', TokenKind.EQUALS_EQUALS)
+      .put('!', TokenKind.NOT_EQUALS)
+      .put('>', TokenKind.GREATER_EQUALS)
+      .put('<', TokenKind.LESS_EQUALS)
+      .put('+', TokenKind.PLUS_EQUALS)
+      .put('-', TokenKind.MINUS_EQUALS)
+      .put('*', TokenKind.STAR_EQUALS)
+      .put('/', TokenKind.SLASH_EQUALS)
+      .put('%', TokenKind.PERCENT_EQUALS)
+      .build();
+
+  private final LexerMode mode;
+
+  // Input buffer and position
+  private final char[] buffer;
+  private int pos;
+
+  private final List<Token> tokens;
+
+  // The number of unclosed open-parens ("(", '{', '[') at the current point in
+  // the stream. Whitespace is handled differently when this is nonzero.
+  private int openParenStackDepth = 0;
+
+  // The stack of enclosing indentation levels; always contains '0' at the
+  // bottom.
+  private final Stack<Integer> indentStack = new Stack<>();
+
+  private boolean containsErrors;
+
+  /**
+   * Constructs a lexer which tokenizes the contents of the specified
+   * InputBuffer. Any errors during lexing are reported on "handler".
+   */
+  public BuildLexerBase(CharSequence input, int initialStackDepth, LexerMode mode) {
+    this.buffer = input.toString().toCharArray();
+    // Empirical measurements show roughly 1 token per 8 characters in buffer.
+    this.tokens = Lists.newArrayListWithExpectedSize(buffer.length / 8);
+    this.pos = 0;
+    this.openParenStackDepth = initialStackDepth;
+    this.mode = mode;
+
+    indentStack.push(0);
+    tokenize();
+  }
+
+  /**
+   * The number of unclosed open-parens ("(", '{', '[') at the end of this string.
+   */
+  public int getOpenParenStackDepth() {
+    return openParenStackDepth;
+  }
+
+  /**
+   * Returns true if there were errors during scanning of this input file or
+   * string. The BuildLexerBase may attempt to recover from errors, but clients should
+   * not rely on the results of scanning if this flag is set.
+   */
+  public boolean containsErrors() {
+    return containsErrors;
+  }
+
+  /**
+   * Returns the (mutable) list of tokens generated by the BuildLexerBase.
+   */
+  public List<Token> getTokens() {
+    return tokens;
+  }
+
+  private void popParen() {
+    if (openParenStackDepth == 0) {
+      error("indentation error");
+    } else {
+      openParenStackDepth--;
+    }
+  }
+
+  private void error(String message) {
+    error(message, pos - 1, pos - 1);
+  }
+
+  protected void error(String message, int start, int end)  {
+    this.containsErrors = true;
+  }
+
+  /** invariant: symbol positions are half-open intervals. */
+  private void addToken(TokenKind kind, int left, int right) {
+    addToken(kind, left, right, null);
+  }
+
+  private void addToken(TokenKind kind, int left, int right, @Nullable Object value) {
+    tokens.add(new Token(kind, left, right, value));
+  }
+
+  /**
+   * Parses an end-of-line sequence, handling statement indentation correctly.
+   *
+   * <p>UNIX newlines are assumed (LF). Carriage returns are always ignored.
+   *
+   * <p>ON ENTRY: 'pos' is the index of the char after '\n'.
+   * ON EXIT: 'pos' is the index of the next non-space char after '\n'.
+   */
+  protected void newline() {
+    if (mode == LexerMode.SyntaxHighlighting) {
+      addToken(TokenKind.NEWLINE, pos - 1, pos);
+      return;
+    }
+    if (openParenStackDepth > 0) {
+      newlineInsideExpression(); // in an expression: ignore space
+    } else {
+      newlineOutsideExpression(); // generate NEWLINE/INDENT/DEDENT tokens
+    }
+  }
+
+  private void newlineInsideExpression() {
+    int oldPos = pos - 1;
+    while (pos < buffer.length) {
+      switch (buffer[pos]) {
+        case ' ': case '\t': case '\r':
+          pos++;
+          break;
+        default:
+          // ignored by the parser
+          addToken(TokenKind.WHITESPACE, oldPos, pos);
+          return;
+      }
+    }
+    addToken(TokenKind.WHITESPACE, oldPos, pos);
+  }
+
+  /**
+   * Handle INDENT and DEDENT within statements.<p>
+   * Note these tokens have zero length -- this is because we can have an arbitrary number of
+   * dedents squeezed into some number of characters, and the size of all the lexical elements
+   * must match the number of characters in the file.
+   */
+  private void newlineOutsideExpression() {
+    int oldPos = pos - 1;
+    if (pos > 1) { // skip over newline at start of file
+      addToken(TokenKind.NEWLINE, oldPos, pos);
+      oldPos = pos;
+    }
+
+    // we're in a stmt: suck up space at beginning of next line
+    int indentLen = 0;
+    while (pos < buffer.length) {
+      char c = buffer[pos];
+      if (c == ' ') {
+        indentLen++;
+        pos++;
+      } else if (c == '\t') {
+        indentLen += 8 - indentLen % 8;
+        pos++;
+      } else if (c == '\n') { // entirely blank line: ignore
+        indentLen = 0;
+        pos++;
+      } else if (c == '#') { // line containing only indented comment
+        if (oldPos != pos) {
+          addToken(TokenKind.WHITESPACE, oldPos, pos);
+          oldPos = pos;
+        }
+        while (pos < buffer.length && c != '\n') {
+          c = buffer[pos++];
+        }
+        addToken(TokenKind.COMMENT, oldPos, pos - 1, bufferSlice(oldPos, pos - 1));
+        oldPos = pos - 1;
+        indentLen = 0;
+      } else { // printing character
+        break;
+      }
+    }
+
+    if (oldPos != pos) {
+      addToken(TokenKind.WHITESPACE, oldPos, pos);
+    }
+    if (pos == buffer.length) {
+      indentLen = 0;
+    } // trailing space on last line
+
+    int peekedIndent = indentStack.peek();
+    if (peekedIndent < indentLen) { // push a level
+      indentStack.push(indentLen);
+      addToken(TokenKind.INDENT, pos, pos);
+
+    } else if (peekedIndent > indentLen) { // pop one or more levels
+      while (peekedIndent > indentLen) {
+        indentStack.pop();
+        addToken(TokenKind.DEDENT, pos, pos);
+        peekedIndent = indentStack.peek();
+      }
+
+      if (peekedIndent < indentLen) {
+        error("indentation error");
+      }
+    }
+  }
+
+  /**
+   * Collapse adjacent whitespace characters into a single token
+   */
+  private void addWhitespace() {
+    int oldPos = pos - 1;
+    while (pos < buffer.length) {
+      switch (buffer[pos]) {
+        case ' ': case '\t': case '\r':
+          pos++;
+          break;
+        default:
+          addToken(TokenKind.WHITESPACE, oldPos, pos, bufferSlice(oldPos, pos));
+          return;
+      }
+    }
+    addToken(TokenKind.WHITESPACE, oldPos, pos, bufferSlice(oldPos, pos));
+  }
+
+  /**
+   * Returns true if current position is in the middle of a triple quote
+   * delimiter (3 x quot), and advances 'pos' by two if so.
+   */
+  private boolean skipTripleQuote(char quot) {
+    if (pos + 1 < buffer.length && buffer[pos] == quot && buffer[pos + 1] == quot) {
+      pos += 2;
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  /**
+   * Scans a string literal delimited by 'quot', containing escape sequences.
+   *
+   * <p>ON ENTRY: 'pos' is 1 + the index of the first delimiter
+   * ON EXIT: 'pos' is 1 + the index of the last delimiter.
+   */
+  private void escapedStringLiteral(char quot, boolean isRaw) {
+    int oldPos = isRaw ? pos - 2 : pos - 1;
+    boolean inTripleQuote = skipTripleQuote(quot);
+
+    // more expensive second choice that expands escaped into a buffer
+    StringBuilder literal = new StringBuilder();
+    while (pos < buffer.length) {
+      char c = buffer[pos];
+      pos++;
+      switch (c) {
+        case '\n':
+          if (inTripleQuote) {
+            literal.append(c);
+            break;
+          } else {
+            error("unterminated string literal at eol", oldPos, pos);
+            addToken(TokenKind.STRING, oldPos, pos, literal.toString());
+            newline();
+            return;
+          }
+        case '\\':
+          if (pos == buffer.length) {
+            error("unterminated string literal at eof", oldPos, pos);
+            addToken(TokenKind.STRING, oldPos, pos, literal.toString());
+            return;
+          }
+          if (isRaw) {
+            // Insert \ and the following character.
+            // As in Python, it means that a raw string can never end with a single \.
+            literal.append('\\');
+            literal.append(buffer[pos]);
+            pos++;
+            break;
+          }
+          c = buffer[pos];
+          pos++;
+          switch (c) {
+            case '\n':
+              // ignore end of line character
+              break;
+            case 'n':
+              literal.append('\n');
+              break;
+            case 'r':
+              literal.append('\r');
+              break;
+            case 't':
+              literal.append('\t');
+              break;
+            case '\\':
+              literal.append('\\');
+              break;
+            case '\'':
+              literal.append('\'');
+              break;
+            case '"':
+              literal.append('"');
+              break;
+            case '0': case '1': case '2': case '3':
+            case '4': case '5': case '6': case '7': { // octal escape
+              int octal = c - '0';
+              if (pos < buffer.length) {
+                c = buffer[pos];
+                if (c >= '0' && c <= '7') {
+                  pos++;
+                  octal = (octal << 3) | (c - '0');
+                  if (pos < buffer.length) {
+                    c = buffer[pos];
+                    if (c >= '0' && c <= '7') {
+                      pos++;
+                      octal = (octal << 3) | (c - '0');
+                    }
+                  }
+                }
+              }
+              literal.append((char) (octal & 0xff));
+              break;
+            }
+            case 'a': case 'b': case 'f': case 'N': case 'u': case 'U': case 'v': case 'x':
+              // exists in Python but not implemented in Blaze => error
+              error("escape sequence not implemented: \\" + c, oldPos, pos);
+              break;
+            default:
+              // unknown char escape => "\literal"
+              literal.append('\\');
+              literal.append(c);
+              break;
+          }
+          break;
+        case '\'':
+        case '"':
+          if (c != quot || (inTripleQuote && !skipTripleQuote(quot))) {
+            // Non-matching quote, treat it like a regular char.
+            literal.append(c);
+          } else {
+            // Matching close-delimiter, all done.
+            addToken(TokenKind.STRING, oldPos, pos, literal.toString());
+            return;
+          }
+          break;
+        default:
+          literal.append(c);
+          break;
+      }
+    }
+    error("unterminated string literal at eof", oldPos, pos);
+    addToken(TokenKind.STRING, oldPos, pos, literal.toString());
+  }
+
+  /**
+   * Scans a string literal delimited by 'quot'.
+   *
+   * <ul>
+   * <li> ON ENTRY: 'pos' is 1 + the index of the first delimiter
+   * <li> ON EXIT: 'pos' is 1 + the index of the last delimiter.
+   * </ul>
+   *
+   * @param isRaw if true, do not escape the string.
+   */
+  private void addStringLiteral(char quot, boolean isRaw) {
+    int oldPos = isRaw ? pos - 2 : pos - 1;
+    int start = pos;
+
+    // Don't even attempt to parse triple-quotes here.
+    if (skipTripleQuote(quot)) {
+      pos -= 2;
+      escapedStringLiteral(quot, isRaw);
+      return;
+    }
+
+    // first quick optimistic scan for a simple non-escaped string
+    while (pos < buffer.length) {
+      char c = buffer[pos++];
+      switch (c) {
+        case '\n':
+          error("unterminated string literal at eol", oldPos, pos);
+          addToken(TokenKind.STRING, oldPos, pos - 1, bufferSlice(start, pos - 1));
+          newline();
+          return;
+        case '\\':
+          if (isRaw) {
+            // skip the next character
+            pos++;
+            break;
+          }
+          // oops, hit an escape, need to start over & build a new string buffer
+          pos = oldPos + 1;
+          escapedStringLiteral(quot, false);
+          return;
+        case '\'':
+        case '"':
+          if (c == quot) {
+            // close-quote, all done.
+            addToken(TokenKind.STRING, oldPos, pos, bufferSlice(start, pos - 1));
+            return;
+          }
+      }
+    }
+
+    error("unterminated string literal at eof", oldPos, pos);
+    addToken(TokenKind.STRING, oldPos, pos, bufferSlice(start, pos));
+  }
+
+  private static final ImmutableMap<String, TokenKind> KEYWORD_MAP = createKeywordMap();
+
+  private static ImmutableMap<String, TokenKind> createKeywordMap() {
+    ImmutableMap.Builder<String, TokenKind> builder = ImmutableMap.builder();
+    for (TokenKind kind : TokenKind.KEYWORDS) {
+      builder.put(kind.toString(), kind);
+    }
+    return builder.build();
+  }
+
+  private TokenKind getTokenKindForIdentfier(String id) {
+    TokenKind kind = KEYWORD_MAP.get(id);
+    return kind == null ? TokenKind.IDENTIFIER : kind;
+  }
+
+  private String scanIdentifier() {
+    int oldPos = pos - 1;
+    while (pos < buffer.length) {
+      switch (buffer[pos]) {
+        case '_':
+        case 'a': case 'b': case 'c': case 'd': case 'e': case 'f':
+        case 'g': case 'h': case 'i': case 'j': case 'k': case 'l':
+        case 'm': case 'n': case 'o': case 'p': case 'q': case 'r':
+        case 's': case 't': case 'u': case 'v': case 'w': case 'x':
+        case 'y': case 'z':
+        case 'A': case 'B': case 'C': case 'D': case 'E': case 'F':
+        case 'G': case 'H': case 'I': case 'J': case 'K': case 'L':
+        case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R':
+        case 'S': case 'T': case 'U': case 'V': case 'W': case 'X':
+        case 'Y': case 'Z':
+        case '0': case '1': case '2': case '3': case '4': case '5':
+        case '6': case '7': case '8': case '9':
+          pos++;
+          break;
+        default:
+          return bufferSlice(oldPos, pos);
+      }
+    }
+    return bufferSlice(oldPos, pos);
+  }
+
+  /**
+   * Scans an identifier or keyword.
+   *
+   * <p>ON ENTRY: 'pos' is 1 + the index of the first char in the identifier.
+   * ON EXIT: 'pos' is 1 + the index of the last char in the identifier.
+   *
+   * @return the identifier or keyword token.
+   */
+  private void addIdentifierOrKeyword() {
+    int oldPos = pos - 1;
+    String id = scanIdentifier();
+    TokenKind kind = getTokenKindForIdentfier(id);
+    addToken(kind, oldPos, pos,
+        (kind == TokenKind.IDENTIFIER) ? id : null);
+  }
+
+  private String scanInteger() {
+    int oldPos = pos - 1;
+    while (pos < buffer.length) {
+      char c = buffer[pos];
+      switch (c) {
+        case 'X': case 'x':
+        case 'a': case 'A':
+        case 'b': case 'B':
+        case 'c': case 'C':
+        case 'd': case 'D':
+        case 'e': case 'E':
+        case 'f': case 'F':
+        case '0': case '1':
+        case '2': case '3':
+        case '4': case '5':
+        case '6': case '7':
+        case '8': case '9':
+          pos++;
+          break;
+        default:
+          return bufferSlice(oldPos, pos);
+      }
+    }
+    return bufferSlice(oldPos, pos);
+  }
+
+  /**
+   * Scans an addInteger literal.
+   *
+   * <p>ON ENTRY: 'pos' is 1 + the index of the first char in the literal.
+   * ON EXIT: 'pos' is 1 + the index of the last char in the literal.
+   */
+  private void addInteger() {
+    int oldPos = pos - 1;
+    String literal = scanInteger();
+
+    final String substring;
+    final int radix;
+    if (literal.startsWith("0x") || literal.startsWith("0X")) {
+      radix = 16;
+      substring = literal.substring(2);
+    } else if (literal.startsWith("0") && literal.length() > 1) {
+      radix = 8;
+      substring = literal.substring(1);
+    } else {
+      radix = 10;
+      substring = literal;
+    }
+
+    int value = 0;
+    try {
+      value = Integer.parseInt(substring, radix);
+    } catch (NumberFormatException e) {
+      error("invalid base-" + radix + " integer constant: " + literal);
+    }
+
+    addToken(TokenKind.INT, oldPos, pos, value);
+  }
+
+  /**
+   * Tokenizes a two-char operator.
+   * @return true if it tokenized an operator
+   */
+  private boolean tokenizeTwoChars() {
+    if (pos + 2 >= buffer.length) {
+      return false;
+    }
+    char c1 = buffer[pos];
+    char c2 = buffer[pos + 1];
+    TokenKind tok = null;
+    if (c2 == '=') {
+      tok = EQUAL_TOKENS.get(c1);
+    } else if (c2 == '*' && c1 == '*') {
+      tok = TokenKind.STAR_STAR;
+    }
+    if (tok == null) {
+      return false;
+    }
+    addToken(tok, pos, pos + 2);
+    return true;
+  }
+
+  /**
+   * Performs tokenization of the character buffer of file contents provided to
+   * the constructor.
+   */
+  private void tokenize() {
+    while (pos < buffer.length) {
+      if (tokenizeTwoChars()) {
+        pos += 2;
+        continue;
+      }
+      char c = buffer[pos];
+      pos++;
+      switch (c) {
+        case '{': {
+          addToken(TokenKind.LBRACE, pos - 1, pos);
+          openParenStackDepth++;
+          break;
+        }
+        case '}': {
+          addToken(TokenKind.RBRACE, pos - 1, pos);
+          popParen();
+          break;
+        }
+        case '(': {
+          addToken(TokenKind.LPAREN, pos - 1, pos);
+          openParenStackDepth++;
+          break;
+        }
+        case ')': {
+          addToken(TokenKind.RPAREN, pos - 1, pos);
+          popParen();
+          break;
+        }
+        case '[': {
+          addToken(TokenKind.LBRACKET, pos - 1, pos);
+          openParenStackDepth++;
+          break;
+        }
+        case ']': {
+          addToken(TokenKind.RBRACKET, pos - 1, pos);
+          popParen();
+          break;
+        }
+        case '>': {
+          addToken(TokenKind.GREATER, pos - 1, pos);
+          break;
+        }
+        case '<': {
+          addToken(TokenKind.LESS, pos - 1, pos);
+          break;
+        }
+        case ':': {
+          addToken(TokenKind.COLON, pos - 1, pos);
+          break;
+        }
+        case ',': {
+          addToken(TokenKind.COMMA, pos - 1, pos);
+          break;
+        }
+        case '+': {
+          addToken(TokenKind.PLUS, pos - 1, pos);
+          break;
+        }
+        case '-': {
+          addToken(TokenKind.MINUS, pos - 1, pos);
+          break;
+        }
+        case '|': {
+          addToken(TokenKind.PIPE, pos - 1, pos);
+          break;
+        }
+        case '=': {
+          addToken(TokenKind.EQUALS, pos - 1, pos);
+          break;
+        }
+        case '%': {
+          addToken(TokenKind.PERCENT, pos - 1, pos);
+          break;
+        }
+        case '/': {
+          addToken(TokenKind.SLASH, pos - 1, pos);
+          break;
+        }
+        case ';': {
+          addToken(TokenKind.SEMI, pos - 1, pos);
+          break;
+        }
+        case '.': {
+          addToken(TokenKind.DOT, pos - 1, pos);
+          break;
+        }
+        case '*': {
+          addToken(TokenKind.STAR, pos - 1, pos);
+          break;
+        }
+        case ' ':
+        case '\t':
+        case '\r': {
+          addWhitespace();
+          break;
+        }
+        case '\\': {
+          // Backslash character is valid only at the end of a line (or in a string)
+          if (pos + 1 < buffer.length && buffer[pos] == '\n') {
+            // treat end of line backslash and newline char as whitespace (they're ignored by the parser)
+            pos++;
+            addToken(TokenKind.WHITESPACE, pos - 2, pos, Character.toString(c));
+          } else {
+            addToken(TokenKind.ILLEGAL, pos - 1, pos, Character.toString(c));
+          }
+          break;
+        }
+        case '\n': {
+          newline();
+          break;
+        }
+        case '#': {
+          int oldPos = pos - 1;
+          while (pos < buffer.length) {
+            c = buffer[pos];
+            if (c == '\n') {
+              break;
+            } else {
+              pos++;
+            }
+          }
+          addToken(TokenKind.COMMENT, oldPos, pos, bufferSlice(oldPos, pos));
+          break;
+        }
+        case '\'':
+        case '\"': {
+          addStringLiteral(c, false);
+          break;
+        }
+        default: {
+          // detect raw strings, e.g. r"str"
+          if (c == 'r' && pos < buffer.length
+              && (buffer[pos] == '\'' || buffer[pos] == '\"')) {
+            c = buffer[pos];
+            pos++;
+            addStringLiteral(c, true);
+            break;
+          }
+
+          if (Character.isDigit(c)) {
+            addInteger();
+          } else if (Character.isJavaIdentifierStart(c) && c != '$') {
+            addIdentifierOrKeyword();
+          } else {
+            // Some characters in Python are not recognized in Blaze syntax (e.g. '!')
+            addToken(TokenKind.ILLEGAL, pos - 1, pos, Character.toString(c));
+            error("invalid character: '" + c + "'");
+          }
+          break;
+        } // default
+      } // switch
+    } // while
+  }
+
+  /**
+   * Returns parts of the source buffer based on offsets
+   *
+   * @param start the beginning offset for the slice
+   * @param end the offset immediately following the slice
+   * @return the text at offset start with length end - start
+   */
+  private String bufferSlice(int start, int end) {
+    return new String(this.buffer, start, end - start);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildToken.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildToken.java
new file mode 100644
index 0000000..2253bca
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/BuildToken.java
@@ -0,0 +1,63 @@
+/*
+ * 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.lexer;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.TokenSet;
+
+/**
+ * The IElementTypes used by the BUILD language
+ */
+public class BuildToken extends IElementType {
+
+
+  private static ImmutableMap<TokenKind, BuildToken> types = createMap();
+
+  private static ImmutableMap<TokenKind, BuildToken> createMap() {
+    ImmutableMap.Builder<TokenKind, BuildToken> builder = ImmutableMap.builder();
+    for (TokenKind kind : TokenKind.values()) {
+      builder.put(kind, new BuildToken(kind));
+    }
+    return builder.build();
+  }
+
+  public static BuildToken fromKind(TokenKind kind) {
+    return types.get(kind);
+  }
+
+  public static final BuildToken IDENTIFIER = fromKind(TokenKind.IDENTIFIER);
+
+  public static final TokenSet WHITESPACE_AND_NEWLINE = TokenSet.create(
+    fromKind(TokenKind.WHITESPACE),
+    fromKind(TokenKind.NEWLINE)
+  );
+
+  public final TokenKind kind;
+
+  private BuildToken(TokenKind kind) {
+    super(kind.name(), BuildFileType.INSTANCE.getLanguage());
+    this.kind = kind;
+  }
+
+  @Override
+  public String toString() {
+    return kind.toString();
+  }
+}
+
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/Token.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/Token.java
new file mode 100644
index 0000000..84e8fe3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/Token.java
@@ -0,0 +1,53 @@
+/*
+ * 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.lexer;
+
+/**
+ * A Token represents an actual lexeme; that is, a lexical unit, its location in
+ * the input text, its lexical kind, and any associated value.
+ */
+public class Token {
+
+  public final TokenKind kind;
+  public final int left;
+  public final int right;
+  public final Object value;
+
+  public Token(TokenKind kind, int left, int right) {
+    this(kind, left, right, null);
+  }
+
+  public Token(TokenKind kind, int left, int right, Object value) {
+    this.kind = kind;
+    this.left = left;
+    this.right = right;
+    this.value = value;
+  }
+
+  /**
+   * Constructs an easy-to-read string representation of token, suitable for use
+   * in user error messages.
+   */
+  @Override
+  public String toString() {
+    if (kind == TokenKind.STRING) {
+      return "\"" + value + "\"";
+    }
+    return value == null ? kind.toString() : value.toString();
+  }
+
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/TokenKind.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/TokenKind.java
new file mode 100644
index 0000000..7ed2e11
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/lexer/TokenKind.java
@@ -0,0 +1,131 @@
+/*
+ * 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.lexer;
+
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * A TokenKind is an enumeration of each different kind of lexical symbol.
+ */
+public enum TokenKind {
+
+  ASSERT("assert"),
+  AND("and"),
+  AS("as"),
+  BREAK("break"),
+  CLASS("class"),
+  COLON(":"),
+  COMMA(","),
+  COMMENT("comment"),
+  CONTINUE("continue"),
+  DEF("def"),
+  DEL("del"),
+  DOT("."),
+  ELIF("elif"),
+  ELSE("else"),
+  EOF("EOF"),
+  EQUALS("="),
+  EQUALS_EQUALS("=="),
+  EXCEPT("except"),
+  FALSE("False"),
+  FINALLY("finally"),
+  FOR("for"),
+  FROM("from"),
+  GLOBAL("global"),
+  GREATER(">"),
+  GREATER_EQUALS(">="),
+  IDENTIFIER("identifier"),
+  IF("if"),
+  ILLEGAL("illegal character"),
+  IMPORT("import"),
+  IN("in"),
+  INDENT("indent"),
+  INT("integer"),
+  IS("is"),
+  LAMBDA("lambda"),
+  LBRACE("{"),
+  LBRACKET("["),
+  LESS("<"),
+  LESS_EQUALS("<="),
+  LOAD("load"),
+  LPAREN("("),
+  MINUS("-"),
+  NEWLINE("newline"),
+  NONLOCAL("nonlocal"),
+  NOT("not"),
+  NOT_EQUALS("!="),
+  NOT_IN("not in"), // used internally by the parser; not directly created by the lexer
+  OR("or"),
+  DEDENT("dedent"),
+  PASS("pass"),
+  PERCENT("%"),
+  PIPE("|"),
+  PLUS("+"),
+  PLUS_EQUALS("+="),
+  MINUS_EQUALS("-="),
+  STAR_EQUALS("*="),
+  SLASH_EQUALS("/="),
+  PERCENT_EQUALS("%="),
+  RAISE("raise"),
+  RBRACE("}"),
+  RBRACKET("]"),
+  RETURN("return"),
+  RPAREN(")"),
+  SEMI(";"),
+  SLASH("/"),
+  STAR("*"),
+  STAR_STAR("**"),
+  STRING("string"),
+  TRUE("True"),
+  TRY("try"),
+  WHILE("while"),
+  WITH("with"),
+  YIELD("yield"),
+  // We need to tokenize all characters.
+  // Whitespace will be used for all tokens which should be ignored by the parser.
+  WHITESPACE("whitespace");
+
+  private final String name;
+
+  TokenKind(String name) {
+    this.name = name;
+  }
+
+  /**
+   * This is a user-friendly name. For keywords (if, yield, True, etc.), it's also
+   * the exact character sequence used by the lexer.
+   */
+  @Override
+  public String toString() {
+    return name;
+  }
+
+  public static ImmutableSet<TokenKind> KEYWORDS = ImmutableSet.of(
+    AND, AS, ASSERT, BREAK, CLASS, CONTINUE, DEF, DEL, ELIF, ELSE, EXCEPT,
+    FALSE, FINALLY, FOR, FROM, GLOBAL, IF, IMPORT, IN, IS, LAMBDA, LOAD,
+    NONLOCAL, NOT, OR, PASS, RAISE, RETURN, TRUE, TRY, WHILE, WITH, YIELD
+  );
+
+  public static ImmutableSet<TokenKind> OPERATIONS = ImmutableSet.of(
+    AND, EQUALS_EQUALS, GREATER, GREATER_EQUALS, IN, LESS, LESS_EQUALS,
+    MINUS, NOT_EQUALS, NOT_IN, OR, PERCENT, SLASH, PLUS, PIPE, STAR
+  );
+
+  public static ImmutableSet<TokenKind> AUGMENTED_ASSIGNMENT_OPS = ImmutableSet.of(
+    PLUS_EQUALS, MINUS_EQUALS, STAR_EQUALS, SLASH_EQUALS, PERCENT_EQUALS
+  );
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserDefinition.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserDefinition.java
new file mode 100644
index 0000000..07c26af
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserDefinition.java
@@ -0,0 +1,119 @@
+/*
+ * 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.parser;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.experiments.DeveloperFlag;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexer;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexerBase;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementType;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementTypes;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.ParserDefinition;
+import com.intellij.lang.PsiBuilder;
+import com.intellij.lang.PsiParser;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.resolve.FileContextUtil;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
+
+/**
+ * Defines the BUILD file parser
+ */
+public class BuildParserDefinition implements ParserDefinition {
+
+  private static final DeveloperFlag DEBUG = new DeveloperFlag("build.file.debug.mode");
+
+  @Override
+  public Lexer createLexer(Project project) {
+    return new BuildLexer(BuildLexerBase.LexerMode.Parsing);
+  }
+
+  @Override
+  public PsiParser createParser(Project project) {
+    return new BuildParser();
+  }
+
+  @Override
+  public IFileElementType getFileNodeType() {
+    return BuildElementTypes.BUILD_FILE;
+  }
+
+  @Override
+  public TokenSet getWhitespaceTokens() {
+    return convert(TokenKind.WHITESPACE, TokenKind.ILLEGAL);
+  }
+
+  @Override
+  public TokenSet getCommentTokens() {
+    return convert(TokenKind.COMMENT);
+  }
+
+  @Override
+  public TokenSet getStringLiteralElements() {
+    return convert(TokenKind.STRING);
+  }
+
+  @Override
+  public PsiElement createElement(ASTNode node) {
+    IElementType type = node.getElementType();
+    if (type instanceof BuildElementType) {
+      return ((BuildElementType) type).createElement(node);
+    }
+    return new ASTWrapperPsiElement(node);
+  }
+
+  @Override
+  public PsiFile createFile(FileViewProvider viewProvider) {
+    return new BuildFile(viewProvider);
+  }
+
+  @Override
+  public SpaceRequirements spaceExistanceTypeBetweenTokens(ASTNode left, ASTNode right) {
+    return SpaceRequirements.MAY;
+  }
+
+  private static TokenSet convert(TokenKind... blazeTokens) {
+    return TokenSet.create(Lists.newArrayList(blazeTokens)
+        .stream()
+        .map(BuildToken::fromKind)
+        .toArray(IElementType[]::new));
+  }
+
+  private static class BuildParser implements PsiParser {
+    @Override
+    public ASTNode parse(IElementType root, PsiBuilder builder) {
+      if (DEBUG.getValue()) {
+        System.err.println(builder.getUserDataUnprotected(FileContextUtil.CONTAINING_FILE_KEY));
+      }
+      PsiBuilder.Marker rootMarker = builder.mark();
+      ParsingContext context = new ParsingContext(builder);
+      context.statementParser.parseFileInput();
+      rootMarker.done(root);
+      return builder.getTreeBuilt();
+    }
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/ExpressionParsing.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/ExpressionParsing.java
new file mode 100644
index 0000000..949e74e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/ExpressionParsing.java
@@ -0,0 +1,543 @@
+/*
+ * 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.parser;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementType;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementTypes;
+import com.intellij.lang.PsiBuilder;
+import com.intellij.openapi.diagnostic.Logger;
+
+import java.util.EnumSet;
+import java.util.List;
+
+/**
+ * For parsing expressions in BUILD files.
+ */
+public class ExpressionParsing extends Parsing {
+
+  private static final Logger LOG = Logger.getInstance("com.google.idea.blaze.base.lang.buildfile.parser.ExpressionParsing");
+
+  private static final ImmutableSet<TokenKind> LIST_TERMINATOR_SET =
+    ImmutableSet.of(
+      TokenKind.EOF,
+      TokenKind.RBRACKET,
+      TokenKind.SEMI);
+
+  private static final ImmutableSet<TokenKind> DICT_TERMINATOR_SET =
+    ImmutableSet.of(
+      TokenKind.EOF,
+      TokenKind.RBRACE,
+      TokenKind.SEMI);
+
+  private static final ImmutableSet<TokenKind> EXPR_LIST_TERMINATOR_SET =
+    ImmutableSet.of(
+      TokenKind.EOF,
+      TokenKind.NEWLINE,
+      TokenKind.EQUALS,
+      TokenKind.RBRACE,
+      TokenKind.RBRACKET,
+      TokenKind.RPAREN,
+      TokenKind.SEMI);
+
+  private static final ImmutableSet<TokenKind> EXPR_TERMINATOR_SET =
+    ImmutableSet.of(
+      TokenKind.EOF,
+      TokenKind.COLON,
+      TokenKind.COMMA,
+      TokenKind.FOR,
+      TokenKind.MINUS,
+      TokenKind.PERCENT,
+      TokenKind.PLUS,
+      TokenKind.RBRACKET,
+      TokenKind.RPAREN,
+      TokenKind.SLASH);
+
+  private static final ImmutableSet<TokenKind> BINARY_OPERATORS =
+    ImmutableSet.of(
+      TokenKind.AND,
+      TokenKind.EQUALS_EQUALS,
+      TokenKind.GREATER,
+      TokenKind.GREATER_EQUALS,
+      TokenKind.IN,
+      TokenKind.LESS,
+      TokenKind.LESS_EQUALS,
+      TokenKind.MINUS,
+      TokenKind.NOT_EQUALS,
+      TokenKind.NOT_IN,
+      TokenKind.OR,
+      TokenKind.PERCENT,
+      TokenKind.SLASH,
+      TokenKind.PLUS,
+      TokenKind.PIPE,
+      TokenKind.STAR);
+
+  private static final ImmutableSet<TokenKind> FUNCALL_TERMINATOR_SET =
+    ImmutableSet.of(
+      TokenKind.EOF,
+      TokenKind.RPAREN,
+      TokenKind.SEMI,
+      TokenKind.NEWLINE);
+
+  /**
+   * Highest precedence goes last.
+   * Based on: http://docs.python.org/2/reference/expressions.html#operator-precedence
+   **/
+  private static final List<EnumSet<TokenKind>> OPERATOR_PRECEDENCE = ImmutableList.of(
+    EnumSet.of(TokenKind.OR),
+    EnumSet.of(TokenKind.AND),
+    EnumSet.of(TokenKind.NOT),
+    EnumSet.of(TokenKind.EQUALS_EQUALS, TokenKind.NOT_EQUALS, TokenKind.LESS, TokenKind.LESS_EQUALS,
+      TokenKind.GREATER, TokenKind.GREATER_EQUALS, TokenKind.IN, TokenKind.NOT_IN),
+    EnumSet.of(TokenKind.PIPE),
+    EnumSet.of(TokenKind.MINUS, TokenKind.PLUS),
+    EnumSet.of(TokenKind.SLASH, TokenKind.STAR, TokenKind.PERCENT));
+
+  public ExpressionParsing(ParsingContext context) {
+    super(context);
+  }
+
+  public void parseExpression(boolean insideParens) {
+    // handle lists without parens (e.g. 'a,b,c = 1')
+    PsiBuilder.Marker marker = insideParens ? null : builder.mark();
+    parseNonTupleExpression();
+    if (currentToken() == TokenKind.COMMA) {
+      parseExpressionList();
+      if (marker != null) {
+        marker.done(BuildElementTypes.LIST_LITERAL);
+      }
+    } else if (marker != null) {
+      marker.drop();
+    }
+  }
+
+  // expr_list ::= ( ',' expr )* ','?
+  private void parseExpressionList() {
+    while (matches(TokenKind.COMMA)) {
+      if (atAnyOfTokens(EXPR_LIST_TERMINATOR_SET)) {
+        break;
+      }
+      parseNonTupleExpression();
+    }
+  }
+
+  protected void parseNonTupleExpression() {
+    parseNonTupleExpression(0);
+    // don't bother including conditional expressions for now, just include their components serially
+    if (matches(TokenKind.IF)) {
+      parseNonTupleExpression(0);
+      if (matches(TokenKind.ELSE)) {
+        parseNonTupleExpression();
+      }
+    }
+  }
+
+  private void parseNonTupleExpression(int prec) {
+    if (prec >= OPERATOR_PRECEDENCE.size()) {
+      parsePrimaryWithSuffix();
+      return;
+    }
+    if (currentToken() == TokenKind.NOT && OPERATOR_PRECEDENCE.get(prec).contains(TokenKind.NOT)) {
+      // special case handling of multi-token 'NOT IN' binary operator
+      if (kindFromElement(builder.lookAhead(1)) != TokenKind.IN) {
+        // skip the 'not' -- no need for a specific 'not' expression
+        builder.advanceLexer();
+        parseNonTupleExpression(prec + 1);
+        return;
+      }
+    }
+    parseBinOpExpression(prec);
+  }
+
+  /**
+   * binop_expression ::= binop_expression OP binop_expression
+   *                    | parsePrimaryWithSuffix
+   * This function takes care of precedence between operators (see OPERATOR_PRECEDENCE for
+   * the order), and it assumes left-to-right associativity.
+   */
+  private void parseBinOpExpression(int prec) {
+    PsiBuilder.Marker marker = builder.mark();
+    parseNonTupleExpression(prec + 1);
+
+    while (true) {
+      if (!atBinaryOperator(prec)) {
+        marker.drop();
+        return;
+      }
+      parseNonTupleExpression(prec + 1);
+      marker.done(BuildElementTypes.BINARY_OP_EXPRESSION);
+      marker = marker.precede();
+    }
+  }
+
+  /**
+   * Consumes current token iff it's a binary operator at the given precedence level
+   * (with special-case handling of 'NOT' 'IN' double token binary operator)
+   */
+  private boolean atBinaryOperator(int prec) {
+    if (matchesAnyOf(OPERATOR_PRECEDENCE.get(prec))) {
+      return true;
+    }
+    if (matchesSequence(TokenKind.NOT, TokenKind.IN)) {
+      return true;
+    }
+    return false;
+  }
+
+  // primary_with_suffix ::= primary (selector_suffix | substring_suffix)*
+  private void parsePrimaryWithSuffix() {
+    PsiBuilder.Marker marker = builder.mark();
+    parsePrimary();
+    while (true) {
+      if (matches(TokenKind.DOT)) {
+        marker = parseSelectorSuffix(marker);
+      } else if (matches(TokenKind.LBRACKET)) {
+        marker = parseSubstringSuffix(marker);
+      } else {
+        break;
+      }
+    }
+    marker.drop();
+  }
+
+  // selector_suffix ::= '.' IDENTIFIER [funcall_suffix]
+  private PsiBuilder.Marker parseSelectorSuffix(PsiBuilder.Marker marker) {
+    if (!atToken(TokenKind.IDENTIFIER)) {
+      builder.error("expected identifier after dot");
+      syncPast(EXPR_TERMINATOR_SET);
+      return marker;
+    }
+    parseTargetOrReferenceIdentifier();
+    if (atToken(TokenKind.LPAREN)) {
+      parseFuncallSuffix();
+      marker.done(BuildElementTypes.FUNCALL_EXPRESSION);
+    } else {
+      marker.done(BuildElementTypes.DOT_EXPRESSION);
+    }
+    return marker.precede();
+  }
+
+  // substring_suffix ::= '[' expression? ':' expression? ':' expression? ']'
+  private PsiBuilder.Marker parseSubstringSuffix(PsiBuilder.Marker marker) {
+    if (!atToken(TokenKind.COLON)) {
+      PsiBuilder.Marker pos = builder.mark();
+      parseExpression(false);
+      pos.done(BuildElementTypes.POSITIONAL);
+    }
+    while (!matches(TokenKind.RBRACKET)) {
+      if (expect(TokenKind.COLON)) {
+        if (!atAnyOfTokens(TokenKind.COLON, TokenKind.RBRACKET)) {
+          parseNonTupleExpression();
+        }
+      } else {
+        syncPast(EXPR_LIST_TERMINATOR_SET);
+        break;
+      }
+    }
+    marker.done(BuildElementTypes.FUNCALL_EXPRESSION);
+    return marker.precede();
+  }
+
+  private void parseTargetOrReferenceIdentifier() {
+    if (!atToken(TokenKind.IDENTIFIER)) {
+      builder.error("expected an identifier");
+      return;
+    }
+    // TODO: handle assigning to a list of targets (e.g. "a,b = 1")
+    TokenKind next = kindFromElement(builder.lookAhead(1));
+    if (next == TokenKind.EQUALS || next == TokenKind.IN) {
+      buildTokenElement(BuildElementTypes.TARGET_EXPRESSION);
+    } else {
+      buildTokenElement(BuildElementTypes.REFERENCE_EXPRESSION);
+    }
+  }
+
+  private void parsePrimary() {
+    TokenKind current = currentToken();
+    switch (current) {
+      case INT:
+        buildTokenElement(BuildElementTypes.INTEGER_LITERAL);
+        return;
+      case STRING:
+        parseStringLiteral(true);
+        return;
+      case IDENTIFIER:
+        PsiBuilder.Marker marker = builder.mark();
+        String tokenText = builder.getTokenText();
+        parseTargetOrReferenceIdentifier();
+        if (atToken(TokenKind.LPAREN)) {
+          parseFuncallSuffix();
+          marker.done(getFuncallExpressionType(tokenText));
+        } else {
+          marker.drop();
+        }
+        return;
+      case TRUE: // intentional fall-through -- both treated as vanilla identifiers
+      case FALSE:
+        buildTokenElement(BuildElementTypes.BOOLEAN_LITERAL);
+        return;
+      case LBRACKET:
+        parseListMaker();
+        return;
+      case LBRACE:
+        parseDictExpression();
+        return;
+      case LPAREN:
+        marker = builder.mark();
+        builder.advanceLexer();
+        if (matches(TokenKind.RPAREN)) {
+          marker.done(BuildElementTypes.LIST_LITERAL);
+          return;
+        }
+        parseExpression(true);
+        expect(TokenKind.RPAREN, true);
+        marker.done(BuildElementTypes.LIST_LITERAL);
+        return;
+      case MINUS:
+        marker = builder.mark();
+        builder.advanceLexer();
+        parsePrimaryWithSuffix();
+        marker.done(BuildElementTypes.POSITIONAL);
+        return;
+      default:
+        builder.error("expected an expression");
+        syncPast(EXPR_TERMINATOR_SET);
+    }
+  }
+
+  /**
+   * funcall_suffix ::= '(' arg_list? ')'
+   * arg_list ::= ((arg ',')* arg ','? )?
+   */
+  private void parseFuncallSuffix() {
+    PsiBuilder.Marker mark = builder.mark();
+    expect(TokenKind.LPAREN, true);
+    if (matches(TokenKind.RPAREN)) {
+      mark.done(BuildElementTypes.ARGUMENT_LIST);
+      return;
+    }
+    parseFuncallArgument();
+    while (!atAnyOfTokens(FUNCALL_TERMINATOR_SET)) {
+      expect(TokenKind.COMMA);
+      if (atAnyOfTokens(FUNCALL_TERMINATOR_SET)) {
+        break;
+      }
+      parseFuncallArgument();
+    }
+    expect(TokenKind.RPAREN, true);
+    mark.done(BuildElementTypes.ARGUMENT_LIST);
+  }
+
+  private BuildElementType getFuncallExpressionType(String functionName) {
+    if ("glob".equals(functionName)) {
+      return BuildElementTypes.GLOB_EXPRESSION;
+    }
+    return BuildElementTypes.FUNCALL_EXPRESSION;
+  }
+
+  protected void parseFunctionParameters() {
+    if (atToken(TokenKind.RPAREN)) {
+      return;
+    }
+    parseFunctionParameter();
+    while (!atAnyOfTokens(FUNCALL_TERMINATOR_SET)) {
+      expect(TokenKind.COMMA);
+      if (atAnyOfTokens(FUNCALL_TERMINATOR_SET)) {
+        break;
+      }
+      parseFunctionParameter();
+    }
+  }
+
+  // arg ::= IDENTIFIER '=' nontupleexpr
+  //       | expr
+  //       | *args
+  //       | **kwargs
+  private void parseFuncallArgument() {
+    PsiBuilder.Marker marker = builder.mark();
+    if (matches(TokenKind.STAR_STAR)) {
+      parseNonTupleExpression();
+      marker.done(BuildElementTypes.STAR_STAR);
+      return;
+    }
+    if (matches(TokenKind.STAR)) {
+      parseNonTupleExpression();
+      marker.done(BuildElementTypes.STAR);
+      return;
+    }
+    if (matchesSequence(TokenKind.IDENTIFIER, TokenKind.EQUALS)) {
+      parseNonTupleExpression();
+      marker.done(BuildElementTypes.KEYWORD);
+      return;
+    }
+    parseNonTupleExpression();
+    marker.done(BuildElementTypes.POSITIONAL);
+  }
+
+  /**
+   * arg ::= IDENTIFIER ['=' nontupleexpr]
+   */
+  private void parseFunctionParameter() {
+    PsiBuilder.Marker marker = builder.mark();
+    if (matches(TokenKind.STAR_STAR)) {
+      expectIdentifier("invalid parameter name");
+      marker.done(BuildElementTypes.PARAM_STAR_STAR);
+      return;
+    }
+    if (matches(TokenKind.STAR)) {
+      if (atToken(TokenKind.IDENTIFIER)) {
+        builder.advanceLexer();
+      }
+      marker.done(BuildElementTypes.PARAM_STAR);
+      return;
+    }
+    expectIdentifier("invalid parameter name");
+    if (matches(TokenKind.EQUALS)) {
+      parseNonTupleExpression();
+      marker.done(BuildElementTypes.PARAM_OPTIONAL);
+      return;
+    }
+    marker.done(BuildElementTypes.PARAM_MANDATORY);
+  }
+
+  protected void expectIdentifier(String error) {
+    expect(TokenKind.IDENTIFIER, error, true);
+  }
+
+  // list_maker ::= '[' ']'
+  //               |'[' expr ']'
+  //               |'[' expr expr_list ']'
+  //               |'[' expr ('FOR' loop_variables 'IN' expr)+ ']'
+  private void parseListMaker() {
+    PsiBuilder.Marker marker = builder.mark();
+    expect(TokenKind.LBRACKET);
+    if (matches(TokenKind.RBRACKET)) {
+      marker.done(BuildElementTypes.LIST_LITERAL);
+      return;
+    }
+    parseNonTupleExpression();
+    switch (currentToken()) {
+      case RBRACKET:
+        builder.advanceLexer();
+        marker.done(BuildElementTypes.LIST_LITERAL);
+        return;
+      case FOR:
+        parseComprehensionSuffix(TokenKind.RBRACKET);
+        marker.done(BuildElementTypes.LIST_COMPREHENSION_EXPR);
+        return;
+      case COMMA:
+        parseExpressionList();
+        if (!matches(TokenKind.RBRACKET)) {
+          builder.error("expected 'for' or ']'");
+          syncPast(LIST_TERMINATOR_SET);
+        }
+        marker.done(BuildElementTypes.LIST_LITERAL);
+        return;
+      default:
+        builder.error("expected ',', 'for' or ']'");
+        syncPast(LIST_TERMINATOR_SET);
+        marker.done(BuildElementTypes.LIST_LITERAL);
+    }
+  }
+
+  // dict_expression ::= '{' '}'
+  //                    |'{' dict_entry_list '}'
+  //                    |'{' dict_entry 'FOR' loop_variables 'IN' expr '}'
+  private void parseDictExpression() {
+    PsiBuilder.Marker marker = builder.mark();
+    expect(TokenKind.LBRACE, true);
+    if (matches(TokenKind.RBRACE)) {
+      marker.done(BuildElementTypes.DICTIONARY_LITERAL);
+      return;
+    }
+    parseDictEntry();
+    if (currentToken() == TokenKind.FOR) {
+      parseComprehensionSuffix(TokenKind.RBRACE);
+      marker.done(BuildElementTypes.LIST_COMPREHENSION_EXPR);
+      return;
+    }
+    if (matches(TokenKind.COMMA)) {
+      parseDictEntryList();
+    }
+    expect(TokenKind.RBRACE, true);
+    marker.done(BuildElementTypes.DICTIONARY_LITERAL);
+  }
+
+  // dict_entry_list ::= ( (dict_entry ',')* dict_entry ','? )?
+  private void parseDictEntryList() {
+    if (atAnyOfTokens(DICT_TERMINATOR_SET)) {
+      return;
+    }
+    parseDictEntry();
+    while (matches(TokenKind.COMMA)) {
+      if (atAnyOfTokens(DICT_TERMINATOR_SET)) {
+        return;
+      }
+      parseDictEntry();
+    }
+  }
+
+  // dict_entry ::= nontupleexpr ':' nontupleexpr
+  private void parseDictEntry() {
+    PsiBuilder.Marker marker = builder.mark();
+    parseNonTupleExpression();
+    expect(TokenKind.COLON);
+    parseNonTupleExpression();
+    marker.done(BuildElementTypes.DICTIONARY_ENTRY_LITERAL);
+  }
+
+  // comprehension_suffix ::= 'FOR' loop_variables 'IN' expr comprehension_suffix
+  //                        | 'IF' expr comprehension_suffix
+  //                        | ']'
+  private void parseComprehensionSuffix(TokenKind closingBracket) {
+    while (true) {
+      if (matches(TokenKind.FOR)) {
+        parseForLoopVariables();
+        expect(TokenKind.IN);
+        parseNonTupleExpression(0);
+      } else if (matches(TokenKind.IF)) {
+        parseExpression(true);
+      } else if (matches(closingBracket)) {
+        return;
+      } else {
+        builder.error("expected " + closingBracket + ", 'for' or 'if'");
+        syncPast(EXPR_LIST_TERMINATOR_SET);
+        return;
+      }
+    }
+  }
+
+  // Equivalent to 'exprlist' rule in Python grammar.
+  // loop_variables ::= primary_with_suffix ( ',' primary_with_suffix )* ','?
+  protected void parseForLoopVariables() {
+    PsiBuilder.Marker marker = builder.mark();
+    parsePrimaryWithSuffix();
+    if (currentToken() != TokenKind.COMMA) {
+      marker.drop();
+      return;
+    }
+    while (matches(TokenKind.COMMA)) {
+      if (atAnyOfTokens(EXPR_LIST_TERMINATOR_SET)) {
+        break;
+      }
+      parsePrimaryWithSuffix();
+    }
+    marker.done(BuildElementTypes.LIST_LITERAL);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/Parsing.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/Parsing.java
new file mode 100644
index 0000000..2188c83
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/Parsing.java
@@ -0,0 +1,255 @@
+/*
+ * 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.parser;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementType;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementTypes;
+import com.intellij.lang.PsiBuilder;
+import com.intellij.psi.tree.IElementType;
+
+import javax.annotation.Nullable;
+import java.util.EnumSet;
+import java.util.Set;
+
+/**
+ * Base class for BUILD file component parsers
+ */
+public abstract class Parsing {
+
+  // Keywords that exist in Python which we don't parse.
+  protected static final EnumSet<TokenKind> FORBIDDEN_KEYWORDS =
+    EnumSet.of(TokenKind.AS, TokenKind.ASSERT,
+      TokenKind.DEL, TokenKind.EXCEPT, TokenKind.FINALLY, TokenKind.FROM, TokenKind.GLOBAL,
+      TokenKind.IMPORT, TokenKind.IS, TokenKind.LAMBDA, TokenKind.NONLOCAL, TokenKind.RAISE,
+      TokenKind.TRY, TokenKind.WITH, TokenKind.WHILE, TokenKind.YIELD);
+
+  protected ParsingContext context;
+  protected PsiBuilder builder;
+
+  public Parsing(ParsingContext context) {
+    this.context = context;
+    this.builder = context.builder;
+  }
+
+  protected ExpressionParsing getExpressionParser() {
+    return context.expressionParser;
+  }
+
+  /**
+   * @return true if a string was parsed
+   */
+  protected boolean parseStringLiteral(boolean alwaysConsume) {
+    if (currentToken() != TokenKind.STRING) {
+      expect(TokenKind.STRING, alwaysConsume);
+      return false;
+    }
+    buildTokenElement(BuildElementTypes.STRING_LITERAL);
+    if (currentToken() == TokenKind.STRING) {
+      builder.error("implicit string concatenation is forbidden; use the '+' operator");
+    }
+    return true;
+  }
+
+  protected void buildTokenElement(BuildElementType type) {
+    PsiBuilder.Marker marker = builder.mark();
+    builder.advanceLexer();
+    marker.done(type);
+  }
+
+  /**
+   * Consume tokens until we reach the first token that has a kind that is in the set of terminatingTokens.
+   */
+  protected void syncTo(Set<TokenKind> terminatingTokens) {
+    // read past the problematic token
+    while (!atAnyOfTokens(terminatingTokens)) {
+      builder.advanceLexer();
+    }
+  }
+
+  /**
+   * Consume tokens until we consume the first token that has a kind that is in the set of terminatingTokens.
+   */
+  protected void syncPast(Set<TokenKind> terminatingTokens) {
+    // read past the problematic token
+    while (!matchesAnyOf(terminatingTokens)) {
+      builder.advanceLexer();
+    }
+  }
+
+  /**
+   * Consumes the current token iff it's one of the expected types.<br>
+   * Otherwise, returns false and reports an error.
+   */
+  protected boolean expect(TokenKind kind) {
+    return expect(kind, false);
+  }
+
+  /**
+   * Consumes the current token if 'alwaysConsume' is true or if it's one of the expected types.<br>
+   * Otherwise, returns false and reports an error.
+   */
+  protected boolean expect(TokenKind kind, boolean alwaysConsume) {
+    return expect(kind, String.format("'%s' expected", kind), alwaysConsume);
+  }
+
+  /**
+   * Consumes the current token if 'alwaysConsume' is true or if it's one of the expected types.<br>
+   * Otherwise, returns false and reports an error.
+   */
+  protected boolean expect(TokenKind kind, String message, boolean alwaysConsume) {
+    TokenKind current = currentToken();
+    if (current == kind || alwaysConsume) {
+      builder.advanceLexer();
+    }
+    if (current != kind) {
+      builder.error(message);
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Checks if we're at the current sequence of tokens. If so, consumes them.
+   */
+  protected boolean matchesSequence(TokenKind... kinds) {
+    PsiBuilder.Marker marker = builder.mark();
+    for (TokenKind kind : kinds) {
+      if (!matches(kind)) {
+        marker.rollbackTo();
+        return false;
+      }
+    }
+    marker.drop();
+    return true;
+  }
+
+  /**
+   * Consumes the current token iff it matches the expected type. Otherwise, returns false
+   */
+  protected boolean matches(TokenKind kind) {
+    if (currentToken() == kind) {
+      builder.advanceLexer();
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Consumes the current token iff it matches one of the expected types. Otherwise, returns false
+   */
+  protected boolean matchesAnyOf(TokenKind... kinds) {
+    TokenKind current = currentToken();
+    for (TokenKind kind : kinds) {
+      if (kind == current) {
+        builder.advanceLexer();
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Consumes the current token iff it's one of the expected types. Otherwise, returns false
+   */
+  protected boolean matchesAnyOf(Set<TokenKind> kinds) {
+    if (kinds.contains(currentToken())) {
+      builder.advanceLexer();
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Checks if the upcoming sequence of tokens match that expected. Doesn't advance the parser.
+   */
+  protected boolean atTokenSequence(TokenKind... kinds) {
+    for (int i = 0; i < kinds.length; i++) {
+      if (kindFromElement(builder.lookAhead(i)) != kinds[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Checks if the current token matches the expected kind. Doesn't advance the parser.
+   */
+  protected boolean atToken(TokenKind kind) {
+    return currentToken() == kind;
+  }
+
+  /**
+   * Checks if the current token matches any one of the expected kinds. Doesn't advance the parser.
+   */
+  protected boolean atAnyOfTokens(TokenKind... kinds) {
+    TokenKind current = currentToken();
+    for (TokenKind kind : kinds) {
+      if (current == kind) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Checks if the current token matches any one of the expected kinds. Doesn't advance the parser.
+   */
+  protected boolean atAnyOfTokens(Set<TokenKind> kinds) {
+    return kinds.contains(currentToken());
+  }
+
+  @Nullable
+  protected TokenKind currentToken() {
+    return builder.eof() ? TokenKind.EOF : kindFromElement(builder.getTokenType());
+  }
+
+  @Nullable
+  protected TokenKind kindFromElement(IElementType type) {
+    if (type == null) {
+      return null;
+    }
+    if (!(type instanceof BuildToken)) {
+      throw new RuntimeException("Invalid type: " + type + " of class " + type.getClass());
+    }
+    TokenKind kind = ((BuildToken) type).kind;
+    checkForbiddenKeywords(kind);
+    return kind;
+  }
+
+  private void checkForbiddenKeywords(TokenKind kind) {
+    if (!FORBIDDEN_KEYWORDS.contains(kind)) {
+      return;
+    }
+    builder.error(forbiddenKeywordError(kind));
+  }
+
+  protected String forbiddenKeywordError(TokenKind kind) {
+    assert FORBIDDEN_KEYWORDS.contains(kind);
+    switch (kind) {
+      case ASSERT: return "'assert' not supported, use 'fail' instead";
+      case TRY: return "'try' not supported, all exceptions are fatal";
+      case IMPORT: return "'import' not supported, use 'load' instead";
+      case IS: return "'is' not supported, use '==' instead";
+      case LAMBDA: return "'lambda' not supported, declare a function instead";
+      case RAISE: return "'raise' not supported, use 'fail' instead";
+      case WHILE: return "'while' not supported, use 'for' instead";
+      default: return "keyword '" + kind + "' not supported";
+    }
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/ParsingContext.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/ParsingContext.java
new file mode 100644
index 0000000..de8e5b7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/ParsingContext.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.lang.buildfile.parser;
+
+import com.intellij.lang.PsiBuilder;
+
+/**
+ * Shared context between BUILD file parsing components
+ */
+public class ParsingContext {
+
+  public final StatementParsing statementParser;
+  public final ExpressionParsing expressionParser;
+  public final PsiBuilder builder;
+
+  public ParsingContext(final PsiBuilder builder) {
+    this.builder = builder;
+    statementParser = new StatementParsing(this);
+    expressionParser = new ExpressionParsing(this);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/StatementParsing.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/StatementParsing.java
new file mode 100644
index 0000000..762db00
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/parser/StatementParsing.java
@@ -0,0 +1,227 @@
+/*
+ * 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.parser;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementTypes;
+import com.intellij.lang.PsiBuilder;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.psi.tree.IElementType;
+
+/**
+ * For parsing statements in BUILD files.
+ */
+public class StatementParsing extends Parsing {
+
+  private static final Logger LOG = Logger.getInstance("com.google.idea.blaze.base.lang.buildfile.parser.StatementParsing");
+
+  private static final ImmutableSet<TokenKind> STATEMENT_TERMINATOR_SET =
+    ImmutableSet.of(TokenKind.EOF, TokenKind.NEWLINE, TokenKind.SEMI);
+
+  private static final ImmutableSet<TokenKind> SMALL_STMT_START =
+    ImmutableSet.of(TokenKind.IDENTIFIER, TokenKind.RETURN);
+
+  public StatementParsing(ParsingContext context) {
+    super(context);
+  }
+
+  /**
+   * Called at the start of parsing. Parses an entire file
+   */
+  public void parseFileInput() {
+    builder.setDebugMode(true);
+    while (!builder.eof()) {
+      if (matches(TokenKind.NEWLINE)) {
+        continue;
+      }
+      parseTopLevelStatement();
+    }
+  }
+
+  // Unlike in Python grammar, 'load' and 'def' are only allowed as a top-level statement
+  public void parseTopLevelStatement() {
+    if (currentToken() == TokenKind.LOAD) {
+      parseLoadStatement();
+    } else if (currentToken() == TokenKind.DEF) {
+      parseFunctionDefStatement();
+    } else {
+      parseStatement();
+    }
+  }
+
+  // simple_stmt | compound_stmt
+  public void parseStatement() {
+    TokenKind current = currentToken();
+    if (current == TokenKind.IF) {
+      parseIfStatement();
+    } else if (current == TokenKind.FOR) {
+      parseForStatement();
+    } else if (FORBIDDEN_KEYWORDS.contains(current)) {
+      PsiBuilder.Marker mark = builder.mark();
+      syncTo(STATEMENT_TERMINATOR_SET);
+      mark.error(forbiddenKeywordError(current));
+      builder.advanceLexer();
+    } else {
+      parseSimpleStatement();
+    }
+  }
+
+  // func_def_stmt ::= DEF IDENTIFIER funcall_suffix ':' suite
+  private void parseFunctionDefStatement() {
+    PsiBuilder.Marker marker = builder.mark();
+    expect(TokenKind.DEF);
+    getExpressionParser().expectIdentifier("expected a function name");
+    PsiBuilder.Marker listMarker = builder.mark();
+    expect(TokenKind.LPAREN);
+    getExpressionParser().parseFunctionParameters();
+    expect(TokenKind.RPAREN, true);
+    listMarker.done(BuildElementTypes.PARAMETER_LIST);
+    expect(TokenKind.COLON);
+    parseSuite();
+    marker.done(BuildElementTypes.FUNCTION_STATEMENT);
+  }
+
+  // load '(' STRING (',' [IDENTIFIER '='] STRING)* [','] ')'
+  private void parseLoadStatement() {
+    PsiBuilder.Marker marker = builder.mark();
+    expect(TokenKind.LOAD);
+    expect(TokenKind.LPAREN);
+    parseStringLiteral(false);
+    // Not implementing [IDENTIFIER EQUALS] option -- not a documented feature, so wait for users to complain
+    boolean hasSymbols = false;
+    while (!matches(TokenKind.RPAREN) && !matchesAnyOf(STATEMENT_TERMINATOR_SET)) {
+      expect(TokenKind.COMMA);
+      if (matches(TokenKind.RPAREN) || matchesAnyOf(STATEMENT_TERMINATOR_SET)) {
+        break;
+      }
+      hasSymbols |= parseStringLiteral(true);
+    }
+    if (!hasSymbols) {
+      builder.error("'load' statements must include at least one loaded function");
+    }
+    marker.done(BuildElementTypes.LOAD_STATEMENT);
+  }
+
+  /**
+   * if_stmt ::= IF expr ':' suite (ELIF expr ':' suite)* [ELSE ':' suite]
+   */
+  private void parseIfStatement() {
+    PsiBuilder.Marker marker = builder.mark();
+    parseIfStatementPart(TokenKind.IF, BuildElementTypes.IF_PART, true);
+    while (currentToken() == TokenKind.ELIF) {
+      parseIfStatementPart(TokenKind.ELIF, BuildElementTypes.ELSE_IF_PART, true);
+    }
+    if (currentToken() == TokenKind.ELSE) {
+      parseIfStatementPart(TokenKind.ELSE, BuildElementTypes.ELSE_PART, false);
+    }
+    marker.done(BuildElementTypes.IF_STATEMENT);
+  }
+
+  // cond_stmts ::= [EL]IF expr ':' suite
+  private void parseIfStatementPart(TokenKind tokenKind, IElementType type, boolean conditional) {
+    PsiBuilder.Marker marker = builder.mark();
+    expect(tokenKind);
+    if (conditional) {
+      getExpressionParser().parseNonTupleExpression();
+    }
+    expect(TokenKind.COLON);
+    parseSuite();
+    marker.done(type);
+  }
+
+  // for_stmt ::= FOR IDENTIFIER IN expr ':' suite
+  private void parseForStatement() {
+    PsiBuilder.Marker marker = builder.mark();
+    expect(TokenKind.FOR);
+    getExpressionParser().parseForLoopVariables();
+    expect(TokenKind.IN);
+    getExpressionParser().parseExpression(false);
+    expect(TokenKind.COLON);
+    parseSuite();
+    marker.done(BuildElementTypes.FOR_STATEMENT);
+  }
+
+  // simple_stmt ::= small_stmt (';' small_stmt)* ';'? NEWLINE
+  private void parseSimpleStatement() {
+    parseSmallStatementOrPass();
+    while (matches(TokenKind.SEMI)) {
+      if (matches(TokenKind.NEWLINE)) {
+        return;
+      }
+      parseSmallStatementOrPass();
+    }
+    if (!builder.eof()) {
+      expect(TokenKind.NEWLINE);
+    }
+  }
+
+  // small_stmt | 'pass'
+  private void parseSmallStatementOrPass() {
+    if (currentToken() == TokenKind.PASS) {
+      buildTokenElement(BuildElementTypes.PASS_STATMENT);
+      return;
+    }
+    parseSmallStatement();
+  }
+
+  private void parseSmallStatement() {
+    if (currentToken() == TokenKind.RETURN) {
+      parseReturnStatement();
+      return;
+    }
+    if (atAnyOfTokens(TokenKind.BREAK, TokenKind.CONTINUE)) {
+      buildTokenElement(BuildElementTypes.FLOW_STATEMENT);
+      return;
+    }
+    PsiBuilder.Marker refMarker = builder.mark();
+    getExpressionParser().parseExpression(false);
+    if (matches(TokenKind.EQUALS)) {
+      getExpressionParser().parseExpression(false);
+      refMarker.done(BuildElementTypes.ASSIGNMENT_STATEMENT);
+    } else if (matchesAnyOf(TokenKind.AUGMENTED_ASSIGNMENT_OPS)) {
+      getExpressionParser().parseExpression(false);
+      refMarker.done(BuildElementTypes.AUGMENTED_ASSIGNMENT);
+    } else {
+      refMarker.drop();
+    }
+  }
+
+  private void parseReturnStatement() {
+    PsiBuilder.Marker marker = builder.mark();
+    expect(TokenKind.RETURN);
+    if (!STATEMENT_TERMINATOR_SET.contains(currentToken())) {
+      getExpressionParser().parseExpression(false);
+    }
+    marker.done(BuildElementTypes.RETURN_STATEMENT);
+  }
+
+  // suite ::= simple_stmt | (NEWLINE INDENT stmt+ DEDENT)
+  private void parseSuite() {
+    if (!matches(TokenKind.NEWLINE)) {
+      parseSimpleStatement();
+      return;
+    }
+    PsiBuilder.Marker marker = builder.mark();
+    if (expect(TokenKind.INDENT)) {
+      while (!builder.eof() && !matches(TokenKind.DEDENT)) {
+        parseStatement();
+      }
+    }
+    marker.done(BuildElementTypes.STATEMENT_LIST);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Argument.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Argument.java
new file mode 100644
index 0000000..16ed4f4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Argument.java
@@ -0,0 +1,125 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.references.ArgumentReference;
+import com.google.idea.blaze.base.lang.buildfile.references.KeywordArgumentReference;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.tree.IElementType;
+
+import javax.annotation.Nullable;
+
+/**
+ * PSI element for an argument, passed via a function call.
+ */
+public abstract class Argument extends BuildElementImpl {
+
+  public static final Argument[] EMPTY_ARRAY = new Argument[0];
+
+  public Argument(ASTNode node) {
+    super(node);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitArgument(this);
+  }
+
+  /**
+   * The value passed by this argument
+   */
+  @Nullable
+  public Expression getValue() {
+    // for *args, **kwargs, this should be 'args' or 'kwargs' identifiers.
+    // otherwise the expression after the (optional) '='
+    ASTNode node = getNode().getLastChildNode();
+    while (node != null) {
+      IElementType type = node.getElementType();
+      if (BuildElementTypes.EXPRESSIONS.contains(type)) {
+        return (Expression) node.getPsi();
+      }
+      if (type == BuildToken.fromKind(TokenKind.EQUALS)
+        || type == BuildToken.fromKind(TokenKind.STAR)
+        || type == BuildToken.fromKind(TokenKind.STAR_STAR)) {
+        break;
+      }
+      node = node.getTreePrev();
+    }
+    return null;
+  }
+
+  public static class Keyword extends Argument {
+    public Keyword(ASTNode node) {
+      super(node);
+    }
+
+    @Override
+    protected void acceptVisitor(BuildElementVisitor visitor) {
+      visitor.visitKeywordArgument(this);
+    }
+
+    @Nullable
+    public ASTNode getNameNode() {
+      return getNode().findChildByType(BuildToken.IDENTIFIER);
+    }
+
+    @Override
+    @Nullable
+    public String getName() {
+      ASTNode node = getNameNode();
+      return node != null ? node.getText() : null;
+    }
+
+    @Override
+    public KeywordArgumentReference getReference() {
+      ASTNode keywordNode = getNameNode();
+      if (keywordNode != null) {
+        TextRange range = PsiUtils.childRangeInParent(getTextRange(), keywordNode.getTextRange());
+        return new KeywordArgumentReference(this, range);
+      }
+      return null;
+    }
+  }
+
+  public static class Positional extends Argument {
+    public Positional(ASTNode node) {
+      super(node);
+    }
+
+    @Override
+    public PsiReference getReference() {
+      return new ArgumentReference<>(this, getTextRange(), true);
+    }
+  }
+
+  public static class Star extends Argument {
+    public Star(ASTNode node) {
+      super(node);
+    }
+  }
+
+  public static class StarStar extends Argument {
+    public StarStar(ASTNode node) {
+      super(node);
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ArgumentList.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ArgumentList.java
new file mode 100644
index 0000000..6c44e32
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ArgumentList.java
@@ -0,0 +1,61 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Argument list of a function call
+ */
+public class ArgumentList extends BuildListType<Argument> {
+
+  public ArgumentList(ASTNode astNode) {
+    super(astNode, Argument.class);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitFuncallArgList(this);
+  }
+
+  public Argument[] getArguments() {
+    return getElements();
+  }
+
+  @Nullable
+  public Argument.Keyword getKeywordArgument(String name) {
+    ASTNode node = getNode().getFirstChildNode();
+    while (node != null) {
+      if (node.getElementType() == BuildElementTypes.KEYWORD) {
+        Argument.Keyword arg = (Argument.Keyword) node.getPsi();
+        String keyword = arg.getName();
+        if (keyword != null && keyword.equals(name)) {
+          return arg;
+        }
+      }
+      node = node.getTreeNext();
+    }
+    return null;
+  }
+
+  @Nullable
+  public Expression getKeywordArgumentValue(String name) {
+    Argument.Keyword keyword = getKeywordArgument(name);
+    return keyword != null ? keyword.getValue() : null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java
new file mode 100644
index 0000000..225ab46
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/AssignmentStatement.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.util.PlatformIcons;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * PSI element for an assignment statement [expr ASSIGN_OP expr]
+ */
+public class AssignmentStatement extends BuildElementImpl implements Statement {
+
+  public AssignmentStatement(ASTNode astNode) {
+    super(astNode);
+  }
+
+  /**
+   * Returns the LHS of the assignment
+   */
+  @Nullable
+  public TargetExpression getLeftHandSideExpression() {
+    return findChildByClass(TargetExpression.class);
+  }
+
+  /**
+   * Returns the RHS of the assignment
+   */
+  @Nullable
+  public Expression getAssignedValue() {
+    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitAssignmentStatement(this);
+  }
+
+  @Nullable
+  @Override
+  public String getName() {
+    TargetExpression target = getLeftHandSideExpression();
+    return target != null ? target.getName() : super.getName();
+  }
+
+  @Override
+  public Icon getIcon(int flags) {
+    return PlatformIcons.FIELD_ICON;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.java
new file mode 100644
index 0000000..acc41cb
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/AugmentedAssignmentStatement.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.lang.buildfile.psi;
+
+import com.intellij.lang.ASTNode;
+
+import javax.annotation.Nullable;
+
+/**
+ * PSI element for an augmented assignment statement [expr += expr]
+ */
+public class AugmentedAssignmentStatement extends BuildElementImpl implements Statement {
+
+  public AugmentedAssignmentStatement(ASTNode astNode) {
+    super(astNode);
+  }
+
+  /**
+   * Returns the LHS of the assignment
+   */
+  @Nullable
+  public TargetExpression getLeftHandSideExpression() {
+    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+  }
+
+  /**
+   * Returns the RHS of the assignment
+   */
+  @Nullable
+  public Expression getAssignedValue() {
+    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitAugmentedAssignmentStatement(this);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java
new file mode 100644
index 0000000..3a35fb1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BinaryOpExpression.java
@@ -0,0 +1,51 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+import javax.annotation.Nullable;
+
+/**
+ * PSI element for an binary operation expression [expr BIN_OP expr]
+ */
+public class BinaryOpExpression extends BuildElementImpl implements Expression {
+
+  public BinaryOpExpression(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitBinaryOpExpression(this);
+  }
+
+  /**
+   * Returns the LHS of the expression
+   */
+  @Nullable
+  public Expression getLhs() {
+    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+  }
+
+  /**
+   * Returns the RHS of the expression
+   */
+  @Nullable
+  public Expression getRhs() {
+    return childToPsi(BuildElementTypes.EXPRESSIONS, 1);
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BooleanLiteral.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BooleanLiteral.java
new file mode 100644
index 0000000..4c8d9f3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BooleanLiteral.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI node for boolean literal expressions
+ */
+public class BooleanLiteral extends BuildElementImpl implements LiteralExpression {
+
+  public BooleanLiteral(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitBooleanLiteral(this);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java
new file mode 100644
index 0000000..8e46c7a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElement.java
@@ -0,0 +1,51 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.intellij.psi.NavigatablePsiElement;
+import com.intellij.psi.PsiElement;
+
+import javax.annotation.Nullable;
+
+/**
+ * Base class for all BUILD file PSI elements
+ */
+public interface BuildElement extends NavigatablePsiElement {
+
+  @Nullable
+  static BuildElement asBuildElement(PsiElement psiElement) {
+    return psiElement instanceof BuildElement ? (BuildElement) psiElement : null;
+  }
+
+  Statement[] EMPTY_ARRAY = new Statement[0];
+
+  String getPresentableText();
+
+  @Nullable
+  PsiElement getReferencedElement();
+
+  <P extends PsiElement> P[] childrenOfClass(Class<P> psiClass);
+
+  <P extends PsiElement> P firstChildOfClass(Class<P> psiClass);
+
+  WorkspacePath getWorkspacePath();
+
+  @Nullable
+  BlazePackage getBlazePackage();
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
new file mode 100644
index 0000000..9195201
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementImpl.java
@@ -0,0 +1,178 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.navigation.ItemPresentation;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiElementVisitor;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.TokenSet;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.lang.reflect.Array;
+import java.util.Arrays;
+
+/**
+ * Base PSI class for the BUILD language
+ */
+public abstract class BuildElementImpl extends ASTWrapperPsiElement implements BuildElement {
+
+  public BuildElementImpl(ASTNode astNode) {
+    super(astNode);
+  }
+
+  public <P extends PsiElement> P getPsiChild(IElementType type, Class<P> psiClass) {
+    ASTNode childNode = getNode().findChildByType(type);
+    return childNode != null ? (P) childNode.getPsi() : null;
+  }
+
+  @Override
+  public <P extends PsiElement> P[] childrenOfClass(Class<P> psiClass) {
+    return findChildrenByClass(psiClass);
+  }
+
+  @Override
+  public <P extends PsiElement> P firstChildOfClass(Class<P> psiClass) {
+    return findChildByClass(psiClass);
+  }
+
+  /**
+   * Returns the BuildElement child at the specified index,
+   * where index is calculated after filtering out non-BuildElement children.
+   * @return null if index >= number of BuildElement children
+   */
+  @Nullable
+  protected BuildElement getBuildElementChild(int index) {
+    BuildElement[] children = buildElementChildren();
+    return children.length <= index ? null : children[index];
+  }
+
+  protected BuildElement[] buildElementChildren() {
+    return Arrays.stream(getNode().getChildren(null))
+      .map(ASTNode::getPsi)
+      .filter(psiElement -> psiElement instanceof BuildElement)
+      .toArray(BuildElement[]::new);
+  }
+
+  protected <T extends BuildElement> T[] childrenToPsi(TokenSet filterSet, T[] array) {
+    final ASTNode[] nodes = getNode().getChildren(filterSet);
+    T[] psiElements = (T[]) Array.newInstance(array.getClass().getComponentType(), nodes.length);
+    for (int i = 0; i < nodes.length; i++) {
+      psiElements[i] = (T) nodes[i].getPsi();
+    }
+    return psiElements;
+  }
+
+  @Nullable
+  protected <T extends BuildElement> T childToPsi(TokenSet filterSet, int index) {
+    final ASTNode[] nodes = getNode().getChildren(filterSet);
+    if (nodes.length <= index) {
+      return null;
+    }
+    return (T) nodes[index].getPsi();
+  }
+
+  @Nullable
+  protected IElementType getParentType() {
+    ASTNode node = getNode().getTreeParent();
+    return node != null ? node.getElementType() : null;
+  }
+
+  public String nonNullName() {
+    String name = getName();
+    return name != null ? name : "<unnamed>";
+  }
+
+  @Override
+  public String getPresentableText() {
+    return nonNullName();
+  }
+
+  @Override
+  public String toString() {
+    return super.toString() + ": " + getPresentableText();
+  }
+
+  @Override
+  public void accept(PsiElementVisitor visitor) {
+    if (visitor instanceof BuildElementVisitor) {
+      acceptVisitor(((BuildElementVisitor) visitor));
+    } else {
+      super.accept(visitor);
+    }
+  }
+
+  protected abstract void acceptVisitor(BuildElementVisitor visitor);
+
+  @Nullable
+  @Override
+  public PsiElement getReferencedElement() {
+    PsiReference[] refs = getReferences();
+    for (PsiReference ref : refs) {
+      PsiElement element = ref.resolve();
+      if (element != null) {
+        return element;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public ItemPresentation getPresentation() {
+    final BuildElement element = this;
+    return new ItemPresentation() {
+      @Override
+      public String getPresentableText() {
+        return element.getPresentableText();
+      }
+      @Override
+      public String getLocationString() {
+        return null;
+      }
+      @Override
+      public Icon getIcon(boolean unused) {
+        return element.getIcon(0);
+      }
+    };
+  }
+
+  @Nullable
+  @Override
+  public WorkspacePath getWorkspacePath() {
+    BuildFile file = (BuildFile) getContainingFile();
+    return file.getWorkspacePath();
+  }
+
+  @Nullable
+  @Override
+  public BlazePackage getBlazePackage() {
+    PsiFile file = getContainingFile();
+    return file != null ? BlazePackage.getContainingPackage(file) : null;
+  }
+
+  @Nullable
+  @Override
+  public BuildFile getContainingFile() {
+    return (BuildFile) super.getContainingFile();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementType.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementType.java
new file mode 100644
index 0000000..a93a210
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementType.java
@@ -0,0 +1,50 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.tree.IElementType;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * IElementTypes used in the AST by the parser (as opposed to the types used by the lexer).<br>
+ * Modelled on IntelliJ's java and python language support conventions.
+ */
+public class BuildElementType extends IElementType {
+
+  private static final Class[] PARAMETER_TYPES = new Class[]{ASTNode.class};
+  private final Class<? extends PsiElement> psiElementClass;
+  private Constructor<? extends PsiElement> constructor;
+
+  public BuildElementType(String name, Class<? extends PsiElement> psiElementClass) {
+    super(name, BuildFileType.INSTANCE.getLanguage());
+    this.psiElementClass = psiElementClass;
+  }
+
+  public PsiElement createElement(ASTNode node) {
+    try {
+      if (constructor == null) {
+        constructor = psiElementClass.getConstructor(PARAMETER_TYPES);
+      }
+      return constructor.newInstance(node);
+    } catch (Exception e) {
+      throw new IllegalStateException("No necessary constructor for " + node.getElementType(), e);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementTypes.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementTypes.java
new file mode 100644
index 0000000..44ead8a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementTypes.java
@@ -0,0 +1,119 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
+
+/**
+ * Collects the types used by the PsiBuilder to construct the AST
+ */
+public interface BuildElementTypes {
+
+  IFileElementType BUILD_FILE = new IFileElementType(BuildFileType.INSTANCE.getLanguage());
+
+  // Statements
+  BuildElementType RETURN_STATEMENT = new BuildElementType("return", ReturnStatement.class);
+  BuildElementType PASS_STATMENT = new BuildElementType("pass", PassStatement.class);
+  BuildElementType ASSIGNMENT_STATEMENT = new BuildElementType("assignment", AssignmentStatement.class);
+  BuildElementType AUGMENTED_ASSIGNMENT = new BuildElementType("aug_assign", AugmentedAssignmentStatement.class);
+  BuildElementType FLOW_STATEMENT = new BuildElementType("flow", FlowStatement.class);
+  BuildElementType LOAD_STATEMENT = new BuildElementType("load", LoadStatement.class);
+  BuildElementType FUNCTION_STATEMENT = new BuildElementType("function_def", FunctionStatement.class);
+  BuildElementType FOR_STATEMENT = new BuildElementType("for", ForStatement.class);
+  BuildElementType IF_STATEMENT = new BuildElementType("if", IfStatement.class);
+
+  BuildElementType IF_PART = new BuildElementType("if_part", IfPart.class);
+  BuildElementType ELSE_IF_PART = new BuildElementType("else_if_part", ElseIfPart.class);
+  BuildElementType ELSE_PART = new BuildElementType("else_part", ElsePart.class);
+
+  BuildElementType STATEMENT_LIST = new BuildElementType("stmt_list", StatementList.class);
+
+  // passed arguments
+  BuildElementType ARGUMENT_LIST = new BuildElementType("arg_list", ArgumentList.class);
+  BuildElementType KEYWORD = new BuildElementType("keyword", Argument.Keyword.class);
+  BuildElementType POSITIONAL = new BuildElementType("positional", Argument.Positional.class);
+  BuildElementType STAR = new BuildElementType("*", Argument.Star.class);
+  BuildElementType STAR_STAR = new BuildElementType("**", Argument.StarStar.class);
+
+  // parameters
+  BuildElementType PARAMETER_LIST = new BuildElementType("parameter_list", ParameterList.class);
+  BuildElementType PARAM_OPTIONAL = new BuildElementType("optional_param", Parameter.Optional.class);
+  BuildElementType PARAM_MANDATORY = new BuildElementType("mandatory_param", Parameter.Mandatory.class);
+  BuildElementType PARAM_STAR = new BuildElementType("*", Parameter.Star.class);
+  BuildElementType PARAM_STAR_STAR = new BuildElementType("**", Parameter.StarStar.class);
+
+  // Expressions
+  BuildElementType DICTIONARY_LITERAL = new BuildElementType("dict", DictionaryLiteral.class);
+  BuildElementType DICTIONARY_ENTRY_LITERAL = new BuildElementType("dict_entry", DictionaryEntryLiteral.class);
+  BuildElementType BINARY_OP_EXPRESSION = new BuildElementType("binary_op", BinaryOpExpression.class);
+  BuildElementType FUNCALL_EXPRESSION = new BuildElementType("function_call", FuncallExpression.class);
+  BuildElementType DOT_EXPRESSION = new BuildElementType("dot_expr", DotExpression.class);
+  BuildElementType STRING_LITERAL = new BuildElementType("string", StringLiteral.class);
+  BuildElementType INTEGER_LITERAL = new BuildElementType("int", IntegerLiteral.class);
+  BuildElementType BOOLEAN_LITERAL = new BuildElementType("bool", BooleanLiteral.class);
+  BuildElementType LIST_LITERAL = new BuildElementType("list", ListLiteral.class);
+  BuildElementType GLOB_EXPRESSION = new BuildElementType("glob", GlobExpression.class);
+  BuildElementType REFERENCE_EXPRESSION = new BuildElementType("reference", ReferenceExpression.class);
+  BuildElementType TARGET_EXPRESSION = new BuildElementType("target", TargetExpression.class);
+  BuildElementType LIST_COMPREHENSION_EXPR = new BuildElementType("list_comp", ListComprehensionExpression.class);
+
+  TokenSet EXPRESSIONS = TokenSet.create(
+    FUNCALL_EXPRESSION,
+    DICTIONARY_LITERAL,
+    DICTIONARY_ENTRY_LITERAL,
+    BINARY_OP_EXPRESSION,
+    DOT_EXPRESSION,
+    STRING_LITERAL,
+    INTEGER_LITERAL,
+    BOOLEAN_LITERAL,
+    LIST_LITERAL,
+    REFERENCE_EXPRESSION,
+    TARGET_EXPRESSION,
+    LIST_COMPREHENSION_EXPR,
+    GLOB_EXPRESSION
+  );
+
+  TokenSet STATEMENTS = TokenSet.create(
+    RETURN_STATEMENT,
+    PASS_STATMENT,
+    ASSIGNMENT_STATEMENT,
+    FLOW_STATEMENT,
+    LOAD_STATEMENT,
+    FUNCTION_STATEMENT,
+    FOR_STATEMENT,
+    IF_STATEMENT
+  );
+
+  TokenSet ARGUMENTS = TokenSet.create(
+    KEYWORD,
+    POSITIONAL,
+    STAR,
+    STAR_STAR
+  );
+
+  TokenSet PARAMETERS = TokenSet.create(
+    PARAM_OPTIONAL,
+    PARAM_MANDATORY,
+    PARAM_STAR,
+    PARAM_STAR_STAR
+  );
+
+  TokenSet STRINGS = TokenSet.create(STRING_LITERAL);
+  TokenSet FUNCTIONS = TokenSet.create(FUNCTION_STATEMENT);
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementVisitor.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementVisitor.java
new file mode 100644
index 0000000..464fa93
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildElementVisitor.java
@@ -0,0 +1,148 @@
+/*
+ * 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.psi;
+
+import com.intellij.psi.PsiElementVisitor;
+
+/**
+ * Visitor for BUILD file PSI nodes
+ */
+public class BuildElementVisitor extends PsiElementVisitor {
+
+  public void visitAssignmentStatement(AssignmentStatement node) {
+    visitElement(node);
+  }
+
+  public void visitAugmentedAssignmentStatement(AugmentedAssignmentStatement node) {
+    visitElement(node);
+  }
+
+  public void visitReturnStatement(ReturnStatement node) {
+    visitElement(node);
+  }
+
+  public void visitArgument(Argument node) {
+    visitElement(node);
+  }
+
+  public void visitKeywordArgument(Argument.Keyword node) {
+    visitElement(node);
+  }
+
+  public void visitParameter(Parameter node) {
+    visitElement(node);
+  }
+
+  public void visitLoadStatement(LoadStatement node) {
+    visitElement(node);
+  }
+
+  public void visitIfStatement(IfStatement node) {
+    visitElement(node);
+  }
+
+  public void visitIfPart(IfPart node) {
+    visitElement(node);
+  }
+
+  public void visitElsePart(ElsePart node) {
+    visitElement(node);
+  }
+
+  public void visitElseIfPart(ElseIfPart node) {
+    visitElement(node);
+  }
+
+  public void visitFunctionStatement(FunctionStatement node) {
+    visitElement(node);
+  }
+
+  public void visitFuncallExpression(FuncallExpression node) {
+    visitElement(node);
+  }
+
+  public void visitForStatement(ForStatement node) {
+    visitElement(node);
+  }
+
+  public void visitFlowStatement(FlowStatement node) {
+    visitElement(node);
+  }
+
+  public void visitDotExpression(DotExpression node) {
+    visitElement(node);
+  }
+
+  public void visitDictionaryLiteral(DictionaryLiteral node) {
+    visitElement(node);
+  }
+
+  public void visitDictionaryEntryLiteral(DictionaryEntryLiteral node) {
+    visitElement(node);
+  }
+
+  public void visitBinaryOpExpression(BinaryOpExpression node) {
+    visitElement(node);
+  }
+
+  public void visitStringLiteral(StringLiteral node) {
+    visitElement(node);
+  }
+
+  public void visitIntegerLiteral(IntegerLiteral node) {
+    visitElement(node);
+  }
+
+  public void visitBooleanLiteral(BooleanLiteral node) {
+    visitElement(node);
+  }
+
+  public void visitListLiteral(ListLiteral node) {
+    visitElement(node);
+  }
+
+  public void visitStatementList(StatementList node) {
+    visitElement(node);
+  }
+
+  public void visitFuncallArgList(ArgumentList node) {
+    visitElement(node);
+  }
+
+  public void visitReferenceExpression(ReferenceExpression node) {
+    visitElement(node);
+  }
+
+  public void visitTargetExpression(TargetExpression node) {
+    visitElement(node);
+  }
+
+  public void visitListComprehensionSuffix(ListComprehensionExpression node) {
+    visitElement(node);
+  }
+
+  public void visitFunctionParameterList(ParameterList node) {
+    visitElement(node);
+  }
+
+  public void visitGlobExpression(GlobExpression node)  {
+    visitElement(node);
+  }
+
+  public void visitPassStatement(PassStatement node) {
+    visitElement(node);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildFile.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildFile.java
new file mode 100644
index 0000000..4c87720
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildFile.java
@@ -0,0 +1,283 @@
+/*
+ * 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.psi;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.lang.buildfile.search.ResolveUtil;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.extapi.psi.PsiFileBase;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import com.intellij.psi.tree.TokenSet;
+import com.intellij.util.PathUtil;
+import com.intellij.util.Processor;
+import icons.BlazeIcons;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.io.File;
+import java.util.List;
+
+/**
+ * Build file PSI element
+ */
+public class BuildFile extends PsiFileBase implements BuildElement {
+
+  public enum BlazeFileType {
+    SkylarkExtension,
+    BuildPackage // "BUILD", plus hacks such as "BUILD.tools", "BUILD.bazel"
+  }
+
+  @Nullable
+  public static WorkspacePath getWorkspacePath(Project project, String filePath) {
+    return BuildReferenceManager.getInstance(project).getWorkspaceRelativePath(filePath);
+  }
+
+  public static String getBuildFileString(Project project, String filePath) {
+    WorkspacePath workspacePath = getWorkspacePath(project, PathUtil.getParentPath(filePath));
+    if (workspacePath == null) {
+      return "BUILD file: " + filePath;
+    }
+    String fileName = PathUtil.getFileName(filePath);
+    if (fileName.startsWith("BUILD")) {
+      return "//" + workspacePath + "/" + fileName;
+    }
+    return "//" + workspacePath + ":" + fileName;
+  }
+
+  public BuildFile(FileViewProvider viewProvider) {
+    super(viewProvider, BuildFileType.INSTANCE.getLanguage());
+  }
+
+  @Override
+  public FileType getFileType() {
+    return BuildFileType.INSTANCE;
+  }
+
+  public BlazeFileType getBlazeFileType() {
+    String fileName = getFileName();
+    if (fileName.startsWith("BUILD")) {
+      return BlazeFileType.BuildPackage;
+    }
+    return BlazeFileType.SkylarkExtension;
+  }
+
+  @Nullable
+  @Override
+  public BlazePackage getBlazePackage() {
+    return BlazePackage.getContainingPackage(this);
+  }
+
+  public String getFileName() {
+    return getViewProvider().getVirtualFile().getName();
+  }
+
+  public String getFilePath() {
+    return getOriginalFile().getViewProvider().getVirtualFile().getPath();
+  }
+
+  public File getFile() {
+    return new File(getFilePath());
+  }
+
+  @Nullable
+  @Override
+  public WorkspacePath getWorkspacePath() {
+    return getWorkspacePath(getProject(), getFilePath());
+  }
+
+  /**
+   * The workspace path of the containing blaze package
+   * (this is always the parent directory for BUILD files, but may be a more distant ancestor for Skylark extensions)
+   */
+  @Nullable
+  public WorkspacePath getPackageWorkspacePath() {
+    BlazePackage parentPackage = getBlazePackage();
+    if (parentPackage == null) {
+      return null;
+    }
+    String filePath = parentPackage.buildFile.getFilePath();
+    return filePath != null ? getWorkspacePath(getProject(), PathUtil.getParentPath(filePath)) : null;
+  }
+
+  @Nullable
+  public String getWorkspaceRelativePackagePath() {
+    WorkspacePath packagePath = getPackageWorkspacePath();
+    return packagePath != null ? packagePath.relativePath() : null;
+  }
+
+  /**
+   * Finds a top-level rule with a "name" keyword argument with the given value.
+   */
+  @Nullable
+  public FuncallExpression findRule(String name) {
+    for (FuncallExpression expr : findChildrenByClass(FuncallExpression.class)) {
+      String ruleName = expr.getNameArgumentValue();
+      if (name.equals(ruleName)) {
+        return expr;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * .bzl files referenced in 'load' statements
+   */
+  @Nullable
+  public String[] getImportedPaths() {
+    ASTNode[] loadStatements = getNode().getChildren(TokenSet.create(BuildElementTypes.LOAD_STATEMENT));
+    if (loadStatements.length == 0) {
+      return null;
+    }
+    List<String> importedPaths = Lists.newArrayListWithCapacity(loadStatements.length);
+    for (int i = 0; i < loadStatements.length; i++) {
+      String path = ((LoadStatement) loadStatements[i].getPsi()).getImportedPath();
+      if (path != null) {
+        importedPaths.add(path);
+      }
+    }
+    return importedPaths.toArray(new String[importedPaths.size()]);
+  }
+
+  @Nullable
+  public FunctionStatement findDeclaredFunction(String name) {
+    for (FunctionStatement fn : getFunctionDeclarations()) {
+      if (name.equals(fn.getName())) {
+        return fn;
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  public TargetExpression findTopLevelVariable(String name) {
+    return ResolveUtil.searchChildAssignmentStatements(this, name);
+  }
+
+  @Nullable
+  public FunctionStatement findLoadedFunction(String name) {
+    for (LoadStatement loadStatement : findChildrenByClass(LoadStatement.class)) {
+      for (StringLiteral importedFunctionNode : loadStatement.getImportedSymbolElements()) {
+        if (name.equals(importedFunctionNode.getStringContents())) {
+          PsiElement element = importedFunctionNode.getReferencedElement();
+          return element instanceof FunctionStatement ? (FunctionStatement) element : null;
+        }
+      }
+    }
+    return null;
+  }
+
+  public BuildElement findSymbolInScope(String name) {
+    BuildElement[] resultHolder = new BuildElement[1];
+    Processor<BuildElement> processor = buildElement -> {
+      if (buildElement instanceof StringLiteral) {
+        buildElement = BuildElement.asBuildElement(buildElement.getReferencedElement());
+      }
+      if (buildElement instanceof PsiNamedElement
+          && name.equals(buildElement.getName())) {
+        resultHolder[0] = buildElement;
+        return false;
+      }
+      return true;
+    };
+    searchSymbolsInScope(processor, null);
+    return resultHolder[0];
+  }
+
+  /**
+   * Iterates over all top-level assignment statements, function definitions and loaded symbols.
+   * @return false if searching was stopped (e.g. because the desired element was found).
+   */
+  public boolean searchSymbolsInScope(Processor<BuildElement> processor, @Nullable PsiElement stopAtElement) {
+    for (BuildElement child : findChildrenByClass(BuildElement.class)) {
+      if (child == stopAtElement) {
+        return true;
+      }
+      if (child instanceof AssignmentStatement) {
+        TargetExpression target = ((AssignmentStatement) child).getLeftHandSideExpression();
+        if (target != null && !processor.process(target)) {
+          return false;
+        }
+      } else if (child instanceof FunctionStatement) {
+        if (!processor.process(child)) {
+          return false;
+        }
+      } else if (child instanceof LoadStatement) {
+        for (StringLiteral importedSymbol : ((LoadStatement) child).getImportedSymbolElements()) {
+          if (!processor.process(importedSymbol)) {
+            return false;
+          }
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Searches functions declared in this file, then loaded Skylark extensions, if relevant.
+   */
+  @Nullable
+  public FunctionStatement findFunctionInScope(String name) {
+    FunctionStatement localFn = findDeclaredFunction(name);
+    if (localFn != null) {
+      return localFn;
+    }
+    return findLoadedFunction(name);
+  }
+
+  public FunctionStatement[] getFunctionDeclarations() {
+    return findChildrenByClass(FunctionStatement.class);
+  }
+
+  @Override
+  public Icon getIcon(int flags) {
+    return BlazeIcons.BuildFile;
+  }
+
+  @Override
+  public String getPresentableText() {
+    return toString();
+  }
+
+  @Override
+  public String toString() {
+    return getBuildFileString(getProject(), getFilePath());
+  }
+
+  @Nullable
+  @Override
+  public PsiElement getReferencedElement() {
+    return null;
+  }
+
+  @Override
+  public <P extends PsiElement> P[] childrenOfClass(Class<P> psiClass) {
+    return findChildrenByClass(psiClass);
+  }
+
+  @Override
+  public <P extends PsiElement> P firstChildOfClass(Class<P> psiClass) {
+    return findChildByClass(psiClass);
+  }
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildListType.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildListType.java
new file mode 100644
index 0000000..2510bc1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/BuildListType.java
@@ -0,0 +1,56 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+import javax.annotation.Nullable;
+
+/**
+ * Common interface for BUILD psi elements containing a list / sequence of child elements.
+ */
+public abstract class BuildListType<E extends BuildElement> extends BuildElementImpl {
+
+  private final Class<E> elementClass;
+
+  public BuildListType(ASTNode astNode, Class<E> elementClass) {
+    super(astNode);
+    this.elementClass = elementClass;
+  }
+
+  public E[] getElements() {
+    return findChildrenByClass(elementClass);
+  }
+
+  @Nullable
+  public E getFirstElement() {
+    return findChildByClass(elementClass);
+  }
+
+  public boolean isEmpty() {
+    return getFirstElement() != null;
+  }
+
+  /**
+   * The offset into the document at which child elements start.
+   * For lists wrapped in braces, this is the offset after the opening brace.
+   * For statement lists, this is the offset after the colon.
+   */
+  public int getStartOffset() {
+    return getNode().getStartOffset() + 1;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DictionaryEntryLiteral.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DictionaryEntryLiteral.java
new file mode 100644
index 0000000..26eb53b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DictionaryEntryLiteral.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for an dictionary entry literal (dictEntry ::= nonTupleExpr ':' nonTupleExpr)
+ */
+public class DictionaryEntryLiteral extends BuildElementImpl implements LiteralExpression {
+
+  public DictionaryEntryLiteral(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitDictionaryEntryLiteral(this);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DictionaryLiteral.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DictionaryLiteral.java
new file mode 100644
index 0000000..2b6cdba
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DictionaryLiteral.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for an dictionary literal.
+ */
+public class DictionaryLiteral extends BuildListType<DictionaryEntryLiteral> implements LiteralExpression {
+
+  public DictionaryLiteral(ASTNode astNode) {
+    super(astNode, DictionaryEntryLiteral.class);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitDictionaryLiteral(this);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DotExpression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DotExpression.java
new file mode 100644
index 0000000..aceedc4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/DotExpression.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for a dot expression.
+ */
+public class DotExpression extends BuildElementImpl implements Expression {
+
+  public DotExpression(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitDotExpression(this);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ElseIfPart.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ElseIfPart.java
new file mode 100644
index 0000000..cfce805
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ElseIfPart.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for an elif part of an IfStatement.
+ */
+public class ElseIfPart extends BuildElementImpl implements Statement, StatementListContainer {
+
+  public ElseIfPart(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitElseIfPart(this);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ElsePart.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ElsePart.java
new file mode 100644
index 0000000..911089a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ElsePart.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for an else part of an IfStatement.
+ */
+public class ElsePart extends BuildElementImpl implements Statement, StatementListContainer {
+
+  public ElsePart(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitElsePart(this);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Expression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Expression.java
new file mode 100644
index 0000000..12b8166
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Expression.java
@@ -0,0 +1,26 @@
+/*
+ * 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.psi;
+
+/**
+ * Interface for all expression PSI elements in a BUILD file
+ */
+public interface Expression extends BuildElement {
+
+  Expression[] EMPTY_ARRAY = new Expression[0];
+
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FlowStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FlowStatement.java
new file mode 100644
index 0000000..e7e93d1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FlowStatement.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for a flow statement.
+ */
+public class FlowStatement extends BuildElementImpl implements Statement {
+
+  public FlowStatement(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitFlowStatement(this);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ForStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ForStatement.java
new file mode 100644
index 0000000..f87744f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ForStatement.java
@@ -0,0 +1,61 @@
+/*
+ * 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.psi;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+
+import java.util.List;
+
+/**
+ * PSI element for a for statement.
+ */
+public class ForStatement extends BuildElementImpl implements Statement, StatementListContainer {
+
+  public ForStatement(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitForStatement(this);
+  }
+
+  public List<Expression> getForLoopVariables() {
+    List<Expression> loopVariableExpressions = Lists.newArrayList();
+    for (PsiElement child : getChildren()) {
+      if (child.getNode().getElementType() == BuildToken.fromKind(TokenKind.IN)) {
+        return loopVariableExpressions;
+      }
+      if (child instanceof Expression) {
+        loopVariableExpressions.add((Expression) child);
+      } else if (child instanceof ListLiteral) {
+        for (Expression expr : ((ListLiteral) child).childrenOfClass(Expression.class)) {
+          loopVariableExpressions.add(expr);
+        }
+      }
+    }
+    return loopVariableExpressions;
+  }
+
+  @Override
+  public String getPresentableText() {
+    return "for loop";
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
new file mode 100644
index 0000000..d8ff4c7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FuncallExpression.java
@@ -0,0 +1,211 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.references.FuncallReference;
+import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNameIdentifierOwner;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.Processor;
+import icons.BlazeIcons;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * PSI element for an function call.<br>
+ * Could be a top-level rule, Skylark function reference, or general some other python function call
+ */
+public class FuncallExpression extends BuildElementImpl implements Expression, PsiNameIdentifierOwner {
+
+  public FuncallExpression(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitFuncallExpression(this);
+  }
+
+  /**
+   * The name of the function being called.
+   */
+  @Nullable
+  public String getFunctionName() {
+    ASTNode node = getFunctionNameNode();
+    return node != null ? node.getText() : null;
+  }
+
+  @Override
+  @Nullable
+  public String getName() {
+    return getNameArgumentValue();
+  }
+
+  @Nullable
+  @Override
+  public PsiElement getNameIdentifier() {
+    Argument.Keyword name = getNameArgument();
+    return name != null ? name.getValue() : null;
+  }
+
+  @Override
+  public PsiElement setName(String name) throws IncorrectOperationException {
+    StringLiteral nameNode = getNameArgumentValueNode();
+    if (nameNode == null) {
+      return this;
+    }
+    ASTNode newChild = PsiUtils.createNewLabel(getProject(), name);
+    nameNode.getNode().replaceChild(nameNode.getNode().getFirstChildNode(), newChild);
+    return this;
+  }
+
+  /**
+   * The function name
+   */
+  @Nullable
+  public ASTNode getFunctionNameNode() {
+    PsiElement argList = getArgList();
+    if (argList != null) {
+      // We want the reference expr directly prior to the open parenthesis.
+      // This accounts for Skylark native.rule calls.
+      PsiElement prev = argList.getPrevSibling();
+      if (prev instanceof ReferenceExpression) {
+        return prev.getNode();
+      }
+    }
+    return getNode().findChildByType(BuildElementTypes.REFERENCE_EXPRESSION);
+  }
+
+  /**
+   * Top-level funcalls are almost always BUILD rules.
+   */
+  public boolean isTopLevel() {
+    ASTNode parent = getNode().getTreeParent();
+    return parent == null || parent.getElementType() == BuildElementTypes.BUILD_FILE;
+  }
+
+  public boolean mightBeBuildRule() {
+    return isTopLevel() || getNameArgument() != null;
+  }
+
+  @Nullable
+  public Label resolveBuildLabel() {
+    return LabelUtils.createLabelFromRuleName(getBlazePackage(), getNameArgumentValue());
+  }
+
+  @Nullable
+  public ArgumentList getArgList() {
+    return findChildByType(BuildElementTypes.ARGUMENT_LIST);
+  }
+
+  public Argument[] getArguments() {
+    ArgumentList argList = getArgList();
+    return argList != null ? argList.getArguments() : Argument.EMPTY_ARRAY;
+  }
+
+  /**
+   * Keyword argument with name "name", if one is present.
+   */
+  @Nullable
+  public Argument.Keyword getNameArgument() {
+    return getKeywordArgument("name");
+  }
+
+  public Argument.Keyword getKeywordArgument(String name) {
+    ArgumentList argList = getArgList();
+    return argList != null ? argList.getKeywordArgument(name) : null;
+  }
+
+  /**
+   * StringLiteral value of keyword argument with name "name", if one is present.
+   */
+  @Nullable
+  public StringLiteral getNameArgumentValueNode() {
+    Argument.Keyword name = getNameArgument();
+    Expression expr = name != null ? name.getValue() : null;
+    if (expr instanceof StringLiteral) {
+      return ((StringLiteral) expr);
+    }
+    return null;
+  }
+
+  /**
+   * Value of keyword argument with name "name", if one is present.
+   */
+  @Nullable
+  public String getNameArgumentValue() {
+    StringLiteral node = getNameArgumentValueNode();
+    return node != null ? node.getStringContents() : null;
+  }
+
+  @Override
+  public Icon getIcon(int flags) {
+    return mightBeBuildRule() ? BlazeIcons.BuildRule : null;
+  }
+
+  @Override
+  public String getPresentableText() {
+    String name = getFunctionName();
+    if (name == null) {
+      return super.getPresentableText();
+    }
+    String targetName = getNameArgumentValue();
+    return targetName != null ? name + "(\"" + targetName + "\")" : name;
+  }
+
+  @Override
+  @Nullable
+  public FuncallReference getReference() {
+    ASTNode nameNode = getFunctionNameNode();
+    if (nameNode == null) {
+      return null;
+    }
+    BuildLanguageSpec spec = BuildLanguageSpecProvider.getInstance().getLanguageSpec(getProject());
+    if (spec != null && spec.hasRule(nameNode.getText())) {
+      // don't try to follow references to built-in rules
+      return null;
+    }
+    TextRange range =  PsiUtils.childRangeInParent(getTextRange(), nameNode.getTextRange());
+    return new FuncallReference(this, range);
+  }
+
+  /**
+   * Searches all StringLiteral children of this element, for one which references the desired target expression.
+   */
+  @Nullable
+  public StringLiteral findChildReferenceToTarget(final FuncallExpression targetRule) {
+    final StringLiteral[] child = new StringLiteral[1];
+    Processor<StringLiteral> processor = stringLiteral -> {
+      PsiElement ref = stringLiteral.getReferencedElement();
+      if (targetRule.equals(ref)) {
+        child[0] = stringLiteral;
+        return false;
+      }
+      return true;
+    };
+    PsiUtils.processChildrenOfType(this, processor, StringLiteral.class);
+    return child[0];
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java
new file mode 100644
index 0000000..ad1dcc8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/FunctionStatement.java
@@ -0,0 +1,59 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * PSI element for a function definition statement.
+ */
+public class FunctionStatement extends NamedBuildElement implements Statement, StatementListContainer {
+
+  public FunctionStatement(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitFunctionStatement(this);
+  }
+
+  @Nullable
+  @Override
+  public Icon getIcon(int flags) {
+    return PlatformIcons.FUNCTION_ICON;
+  }
+
+  @Nullable
+  public ParameterList getParameterList() {
+    return getPsiChild(BuildElementTypes.PARAMETER_LIST, ParameterList.class);
+  }
+
+  public Parameter[] getParameters() {
+    ParameterList list = getParameterList();
+    return list != null ? list.getElements() : Parameter.EMPTY_ARRAY;
+  }
+
+  @Override
+  public String getPresentableText() {
+    return nonNullName() + getParameterList().getPresentableText();
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/GlobExpression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/GlobExpression.java
new file mode 100644
index 0000000..622e427
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/GlobExpression.java
@@ -0,0 +1,125 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.references.GlobReference;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+
+import javax.annotation.Nullable;
+
+/**
+ * PSI element for a glob expression.
+ */
+public class GlobExpression extends BuildElementImpl implements Expression {
+
+  public GlobExpression(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitGlobExpression(this);
+  }
+
+  @Nullable
+  public ArgumentList getArgList() {
+    return findChildByType(BuildElementTypes.ARGUMENT_LIST);
+  }
+
+  public Argument[] getArguments() {
+    ArgumentList argList = getArgList();
+    return argList != null ? argList.getArguments() : Argument.EMPTY_ARRAY;
+  }
+
+  @Nullable
+  public Argument.Keyword getKeywordArgument(String name) {
+    ArgumentList list = getArgList();
+    return list != null ? list.getKeywordArgument(name) : null;
+  }
+
+  @Nullable
+  public Expression getIncludes() {
+    Argument arg = getKeywordArgument("include");
+    if (arg == null) {
+      Argument[] allArgs = getArguments();
+      if (allArgs.length != 0 && allArgs[0] instanceof Argument.Positional) {
+        arg = allArgs[0];
+      }
+    }
+    return getArgValue(arg);
+  }
+
+  @Nullable
+  public Expression getExcludes() {
+    return getArgValue(getKeywordArgument("exclude"));
+  }
+
+  @Nullable
+  private static Expression getArgValue(@Nullable Argument arg) {
+    return arg != null ? arg.getValue() : null;
+  }
+
+  public boolean areDirectoriesExcluded() {
+    Argument.Keyword arg = getKeywordArgument("exclude_directories");
+    if (arg != null) {
+      // '0' and '1' are the only accepted values
+      Expression value = arg.getValue();
+      return value == null || !value.getText().equals("0");
+    }
+    return true;
+  }
+
+  @Nullable
+  public ASTNode getGlobFuncallElement() {
+    return getNode().findChildByType(BuildElementTypes.REFERENCE_EXPRESSION);
+  }
+
+  private volatile GlobReference reference = null;
+
+  @Override
+  public GlobReference getReference() {
+    GlobReference ref = reference;
+    if (ref != null) {
+      return ref;
+    }
+    synchronized (this) {
+      if (reference == null) {
+        reference = new GlobReference(this);
+      }
+      return reference;
+    }
+  }
+
+  /**
+   * The text range within the glob expression used for references.
+   * This is the text the user needs to click on for navigation support,
+   * and also the destination when finding usages in a glob.
+   */
+  public TextRange getReferenceTextRange() {
+    // Ideally, this would be either the full range of the expression, or the range of the specific pattern matching
+    // a given file. However, that leads to conflicts with the individual string references, causing unnecessary
+    // and expensive de-globbing.
+    // e.g. while typing the glob patterns, IJ will be looking for code-completion possibilities, and need to
+    // de-glob to do this (due to a lack of communication between the different code-completion components).
+
+    return new TextRange(0, 4);
+  }
+
+  public boolean matches(String packageRelativePath, boolean isDirectory) {
+    return getReference().matches(packageRelativePath, isDirectory);
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IfPart.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IfPart.java
new file mode 100644
index 0000000..a2477d2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IfPart.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for an if part of an IfStatement.
+ */
+public class IfPart extends BuildElementImpl implements Statement, StatementListContainer {
+
+  public IfPart(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitIfPart(this);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IfStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IfStatement.java
new file mode 100644
index 0000000..4747ca8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IfStatement.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for an if statement.
+ */
+public class IfStatement extends BuildElementImpl implements Statement {
+
+  public IfStatement(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitIfStatement(this);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IntegerLiteral.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IntegerLiteral.java
new file mode 100644
index 0000000..d9bb2ee
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/IntegerLiteral.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI node for integer literal expressions
+ */
+public class IntegerLiteral extends BuildElementImpl implements LiteralExpression {
+
+  public IntegerLiteral(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitIntegerLiteral(this);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ListComprehensionExpression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ListComprehensionExpression.java
new file mode 100644
index 0000000..c341550
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ListComprehensionExpression.java
@@ -0,0 +1,34 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for a list comprehension expression
+ * (comprehension suffix of: '[' expr ('FOR' loop_variables 'IN' expr)+ ']')
+ */
+public class ListComprehensionExpression extends BuildElementImpl implements Expression {
+
+  public ListComprehensionExpression(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitListComprehensionSuffix(this);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ListLiteral.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ListLiteral.java
new file mode 100644
index 0000000..d9b0247
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ListLiteral.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.lang.buildfile.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * PSI element for list and tuple literals
+ */
+public class ListLiteral extends BuildListType<Expression> implements LiteralExpression {
+
+  public ListLiteral(ASTNode astNode) {
+    super(astNode, Expression.class);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitListLiteral(this);
+  }
+
+  @Override
+  public String getPresentableText() {
+    return "list";
+  }
+
+  public Expression[] getChildExpressions() {
+    return findChildrenByClass(Expression.class);
+  }
+
+  @Override
+  public Expression[] getElements() {
+    return getChildExpressions();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return findChildByClass(Expression.class) != null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/LiteralExpression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/LiteralExpression.java
new file mode 100644
index 0000000..f5c7f20
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/LiteralExpression.java
@@ -0,0 +1,22 @@
+/*
+ * 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.psi;
+
+/**
+ * Interface for literal expressions.
+ */
+public interface LiteralExpression extends Expression {
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/LoadStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/LoadStatement.java
new file mode 100644
index 0000000..34fe7e7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/LoadStatement.java
@@ -0,0 +1,107 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
+import com.intellij.lang.ASTNode;
+import com.intellij.util.PlatformIcons;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.util.Arrays;
+
+/**
+ * PSI element for a load statement.
+ */
+public class LoadStatement extends BuildElementImpl implements Statement {
+
+  public LoadStatement(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitLoadStatement(this);
+  }
+
+  @Nullable
+  public ASTNode getImportNode() {
+    return getNode().findChildByType(BuildElementTypes.STRING_LITERAL);
+  }
+
+  @Nullable
+  public StringLiteral getImportPsiElement() {
+    return findChildByType(BuildElementTypes.STRING_LITERAL);
+  }
+
+  @Nullable
+  public String getImportedPath() {
+    ASTNode firstString = getImportNode();
+    return firstString != null ? StringLiteral.parseStringContents(firstString.getText()) : null;
+  }
+
+  /**
+   * The string nodes referencing imported functions.
+   */
+  public FunctionStatement[] getImportedFunctionReferences() {
+    return Arrays.stream(getChildStrings())
+      .skip(1)
+      .map(BuildElement::getReferencedElement)
+      .filter(e -> e instanceof FunctionStatement)
+      .toArray(FunctionStatement[]::new);
+  }
+
+  /**
+   * The string nodes referencing imported functions.
+   */
+  public StringLiteral[] getImportedSymbolElements() {
+    StringLiteral[] childStrings = getChildStrings();
+    return childStrings.length < 2 ? new StringLiteral[0] : Arrays.copyOfRange(childStrings, 1, childStrings.length);
+  }
+
+  public String[] getImportedSymbolNames() {
+    return Arrays.stream(getImportedSymbolElements())
+      .map(StringLiteral::getStringContents)
+      .toArray(String[]::new);
+  }
+
+  @Nullable
+  public StringLiteral findImportedSymbolElement(String name) {
+    for (StringLiteral string : getImportedSymbolElements()) {
+      if (name.equals(string.getStringContents())) {
+        return string;
+      }
+    }
+    return null;
+  }
+
+  public StringLiteral[] getChildStrings() {
+    return Arrays.stream(getNode().getChildren(BuildElementTypes.STRINGS))
+      .map(ASTNode::getPsi)
+      .toArray(StringLiteral[]::new);
+  }
+
+  @Override
+  public Icon getIcon(int flags) {
+    return PlatformIcons.IMPORT_ICON;
+  }
+
+  @Override
+  public String getPresentableText() {
+    String path = LabelUtils.getNiceSkylarkFileName(getImportedPath());
+    return path != null ? "load: " + path : "load";
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/NamedBuildElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/NamedBuildElement.java
new file mode 100644
index 0000000..3de500a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/NamedBuildElement.java
@@ -0,0 +1,75 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNameIdentifierOwner;
+
+import javax.annotation.Nullable;
+
+/**
+ * Base class for PsiNamedElements in BUILD files.
+ */
+public abstract class NamedBuildElement extends BuildElementImpl implements PsiNameIdentifierOwner {
+
+  public NamedBuildElement(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Nullable
+  public ASTNode getNameNode() {
+    return getNode().findChildByType(BuildToken.IDENTIFIER);
+  }
+
+  @Override
+  @Nullable
+  public String getName() {
+    ASTNode node = getNameNode();
+    return node != null ? node.getText() : null;
+  }
+
+  @Override
+  @Nullable
+  public PsiElement getNameIdentifier() {
+    final ASTNode nameNode = getNameNode();
+    return nameNode != null ? nameNode.getPsi() : null;
+  }
+
+  @Override
+  public PsiElement setName(String name) {
+    final ASTNode nameElement = PsiUtils.createNewName(getProject(), name);
+    final ASTNode nameNode = getNameNode();
+    if (nameNode != null) {
+      getNode().replaceChild(nameNode, nameElement);
+    }
+    return this;
+  }
+
+  @Override
+  public int getTextOffset() {
+    final ASTNode name = getNameNode();
+    return name != null ? name.getStartOffset() : super.getTextOffset();
+  }
+
+  @Override
+  public String toString() {
+    return super.toString() + "('" + getName() + "')";
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java
new file mode 100644
index 0000000..efe5822
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Parameter.java
@@ -0,0 +1,125 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.intellij.icons.AllIcons;
+import com.intellij.lang.ASTNode;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * PSI nodes for parameters in a function declaration
+ */
+public abstract class Parameter extends NamedBuildElement {
+
+  public static final Parameter[] EMPTY_ARRAY = new Parameter[0];
+
+  public Parameter(ASTNode node) {
+    super(node);
+  }
+
+  public boolean hasDefaultValue() {
+    return false;
+  }
+
+  @Nullable
+  @Override
+  public Icon getIcon(int flags) {
+    return AllIcons.Nodes.Parameter;
+  }
+
+  /**
+   * Includes stars where relevant.
+   */
+  public String getPresentableName() {
+    return getName();
+  }
+
+  public static class Optional extends Parameter {
+    public Optional(ASTNode node) {
+      super(node);
+    }
+    @Override
+    protected void acceptVisitor(BuildElementVisitor visitor) {
+      visitor.visitParameter(this);
+    }
+
+    public Expression getDefaultValue() {
+      ASTNode node = getNode().getLastChildNode();
+      while (node != null) {
+        if (BuildElementTypes.EXPRESSIONS.contains(node.getElementType())) {
+          return (Expression) node.getPsi();
+        }
+        if (node.getElementType() == BuildToken.fromKind(TokenKind.EQUALS)) {
+          break;
+        }
+        node = node.getTreePrev();
+      }
+      return null;
+    }
+
+    @Override
+    public boolean hasDefaultValue() {
+      return true;
+    }
+  }
+
+  public static class Mandatory extends Parameter {
+    public Mandatory(ASTNode node) {
+      super(node);
+    }
+    @Override
+    protected void acceptVisitor(BuildElementVisitor visitor) {
+      visitor.visitParameter(this);
+    }
+  }
+
+  public static class Star extends Parameter {
+    public Star(ASTNode node) {
+      super(node);
+    }
+
+    @Override
+    protected void acceptVisitor(BuildElementVisitor visitor) {
+      visitor.visitParameter(this);
+    }
+
+    @Override
+    public String getPresentableName() {
+      return "*" + getName();
+    }
+  }
+
+  public static class StarStar extends Parameter {
+    public StarStar(ASTNode node) {
+      super(node);
+    }
+
+    @Override
+    protected void acceptVisitor(BuildElementVisitor visitor) {
+      visitor.visitParameter(this);
+    }
+
+    @Override
+    public String getPresentableName() {
+      return "**" + getName();
+    }
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ParameterList.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ParameterList.java
new file mode 100644
index 0000000..623baa8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ParameterList.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.psi;
+
+import com.intellij.lang.ASTNode;
+
+import javax.annotation.Nullable;
+import java.util.StringJoiner;
+
+/**
+ * Parameter list in a function declaration
+ */
+public class ParameterList extends BuildListType<Parameter> {
+
+  public ParameterList(ASTNode astNode) {
+    super(astNode, Parameter.class);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitFunctionParameterList(this);
+  }
+
+  @Nullable
+  public Parameter findParameterByName(String name) {
+    ASTNode node = getNode().getFirstChildNode();
+    while (node != null) {
+      if (node.getElementType() == BuildElementTypes.PARAM_OPTIONAL
+        || node.getElementType() == BuildElementTypes.PARAM_MANDATORY) {
+        Parameter param = (Parameter) node.getPsi();
+        if (name.equals(param.getName())) {
+          return param;
+        }
+      }
+      node = node.getTreeNext();
+    }
+    return null;
+  }
+
+  public boolean hasStarStar() {
+    return !findChildrenByType(BuildElementTypes.PARAM_STAR_STAR).isEmpty();
+  }
+
+  public boolean hasStar() {
+    return !findChildrenByType(BuildElementTypes.PARAM_STAR).isEmpty();
+  }
+
+  @Override
+  public String getPresentableText() {
+    StringJoiner joiner = new StringJoiner(", ", "(", ")");
+    for (Parameter param : getElements()) {
+      joiner.add(param.getPresentableName());
+    }
+    return joiner.toString();
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/PassStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/PassStatement.java
new file mode 100644
index 0000000..ec1f01f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/PassStatement.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.lang.buildfile.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * Psi element for pass statement.
+ */
+public class PassStatement extends BuildElementImpl implements Statement {
+
+  public PassStatement(ASTNode node) {
+    super(node);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitPassStatement(this);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java
new file mode 100644
index 0000000..198a778
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReferenceExpression.java
@@ -0,0 +1,72 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.references.LocalReference;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * References a PsiNamedElement
+ */
+public class ReferenceExpression extends BuildElementImpl implements Expression {
+
+  public ReferenceExpression(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitReferenceExpression(this);
+  }
+
+  @Nullable
+  public ASTNode getNameElement() {
+    return getNode().findChildByType(BuildToken.IDENTIFIER);
+  }
+
+  @Nullable
+  public String getReferencedName() {
+    ASTNode node = getNameElement();
+    return node != null ? node.getText() : null;
+  }
+
+  @Override
+  public PsiReference getReference() {
+    IElementType parentType = getParentType();
+    // function names are resolved by the parent funcall node
+    if (BuildElementTypes.FUNCALL_EXPRESSION.equals(parentType)) {
+      return null;
+    }
+    if (BuildElementTypes.DOT_EXPRESSION.equals(parentType) && afterDot(getNode())) {
+      return null;
+    }
+    return new LocalReference(this);
+  }
+
+  @Override
+  public String getName() {
+    return getReferencedName();
+  }
+
+  private static boolean afterDot(ASTNode node) {
+    ASTNode prev = node.getTreePrev();
+    return prev != null && prev.getText().equals(".");
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java
new file mode 100644
index 0000000..bde88a3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/ReturnStatement.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.psi;
+
+import com.intellij.lang.ASTNode;
+
+import javax.annotation.Nullable;
+
+/**
+ * A wrapper Statement class for return expressions.
+ */
+public class ReturnStatement extends BuildElementImpl implements Statement {
+
+  public ReturnStatement(ASTNode node) {
+    super(node);
+  }
+
+  @Nullable
+  public Expression getReturnExpression() {
+    return childToPsi(BuildElementTypes.EXPRESSIONS, 0);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitReturnStatement(this);
+  }
+}
+
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Statement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Statement.java
new file mode 100644
index 0000000..e76c2b5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/Statement.java
@@ -0,0 +1,25 @@
+/*
+ * 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.psi;
+
+/**
+ * Base class for all statements nodes in the PSI tree
+ */
+public interface Statement extends BuildElement {
+
+  Statement[] EMPTY_ARRAY = new Statement[0];
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StatementList.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StatementList.java
new file mode 100644
index 0000000..d64a99a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StatementList.java
@@ -0,0 +1,46 @@
+/*
+ * 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.psi;
+
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+
+/**
+ * PSI element for a list of statements
+ */
+public class StatementList extends BuildListType<Statement> {
+
+  public StatementList(ASTNode astNode) {
+    super(astNode, Statement.class);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitStatementList(this);
+  }
+
+  @Override
+  public int getStartOffset() {
+    PsiElement prevSibling = getPrevSibling();
+    while (prevSibling != null) {
+      if (prevSibling.getText().equals(":")) {
+        return prevSibling.getNode().getStartOffset() + 1;
+      }
+      prevSibling = prevSibling.getPrevSibling();
+    }
+    return getNode().getStartOffset();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StatementListContainer.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StatementListContainer.java
new file mode 100644
index 0000000..c640b0a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StatementListContainer.java
@@ -0,0 +1,23 @@
+/*
+ * 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.psi;
+
+/**
+ * Anything of the form type ':' suite
+ */
+public interface StatementListContainer extends BuildElement {
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StringLiteral.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StringLiteral.java
new file mode 100644
index 0000000..e6f05e4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/StringLiteral.java
@@ -0,0 +1,134 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.references.LabelReference;
+import com.google.idea.blaze.base.lang.buildfile.references.LoadedSymbolReference;
+import com.google.idea.blaze.base.lang.buildfile.references.PackageReferenceFragment;
+import com.google.idea.blaze.base.lang.buildfile.references.QuoteType;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+
+import javax.annotation.Nullable;
+
+/**
+ * PSI node for string literal expressions
+ */
+public class StringLiteral extends BuildElementImpl implements LiteralExpression {
+
+  public static String stripEndpointQuotes(ASTNode node) {
+    assert(node.getElementType() == BuildElementTypes.STRING_LITERAL);
+    return parseStringContents(node.getText());
+  }
+
+  /**
+   * Removes the leading and trailing quotes. Naive implementation intended for resolving references
+   * (in which case escaped characters, raw strings, etc. are unlikely).
+   */
+  public static String parseStringContents(String string) {
+    // TODO: Handle escaped characters, etc. here? (extract logic from BuildLexerBase.addStringLiteral)
+    if (string.startsWith("\"\"\"") || string.startsWith("'''")) {
+      return string.length() < 6 ? "" : string.substring(3, string.length() - 3);
+    }
+    return string.length() < 2 ? "" : string.substring(1, string.length() - 1);
+  }
+
+  public static QuoteType getQuoteType(@Nullable String rawText) {
+    if (rawText == null) {
+      return QuoteType.NoQuotes;
+    }
+    if (rawText.startsWith("\"\"\"")) {
+      return QuoteType.TripleDouble;
+    }
+    if (rawText.startsWith("'''")) {
+      return QuoteType.TripleSingle;
+    }
+    if (rawText.startsWith("'")) {
+      return QuoteType.Single;
+    }
+    if (rawText.startsWith("\"")) {
+      return QuoteType.Double;
+    }
+    return QuoteType.NoQuotes;
+  }
+
+  public StringLiteral(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitStringLiteral(this);
+  }
+
+  /**
+   * Removes the leading and trailing quotes
+   */
+  public String getStringContents() {
+    return parseStringContents(getText());
+  }
+
+  public QuoteType getQuoteType() {
+    return getQuoteType(getText());
+  }
+
+  /**
+   * Labels are taken to reference:
+   *   - the actual target they reference
+   *   - the BUILD package specified before the colon (only if explicitly present)
+   */
+  @Override
+  public PsiReference[] getReferences() {
+    PsiReference primaryReference = getReference();
+    if (primaryReference instanceof LabelReference) {
+      return new PsiReference[] {primaryReference, new PackageReferenceFragment((LabelReference) primaryReference)};
+    }
+    return primaryReference != null ? new PsiReference[] {primaryReference} : PsiReference.EMPTY_ARRAY;
+  }
+
+  /**
+   * The primary reference -- this is the target referenced by the full label
+   */
+  @Nullable
+  @Override
+  public PsiReference getReference() {
+    PsiElement parent = getParent();
+    if (parent instanceof LoadStatement) {
+      LoadStatement load = (LoadStatement) parent;
+      StringLiteral importNode = load.getImportPsiElement();
+      if (importNode == null) {
+        return null;
+      }
+      LabelReference importReference = new LabelReference(importNode, false);
+      if (this.equals(importNode)) {
+        return importReference;
+      }
+      return new LoadedSymbolReference(this, importReference);
+    }
+    return new LabelReference(this, true);
+  }
+
+  public boolean insideLoadStatement() {
+    return getParentType() == BuildElementTypes.LOAD_STATEMENT;
+  }
+
+  @Override
+  public String getPresentableText() {
+    return getText();
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java
new file mode 100644
index 0000000..d172ede
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/TargetExpression.java
@@ -0,0 +1,51 @@
+/*
+ * 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.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.references.TargetReference;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiReference;
+import com.intellij.util.PlatformIcons;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * References a PsiNamedElement
+ */
+public class TargetExpression extends NamedBuildElement implements Expression {
+
+  public TargetExpression(ASTNode astNode) {
+    super(astNode);
+  }
+
+  @Override
+  protected void acceptVisitor(BuildElementVisitor visitor) {
+    visitor.visitTargetExpression(this);
+  }
+
+  @Override
+  public PsiReference getReference() {
+    return new TargetReference(this);
+  }
+
+  @Nullable
+  @Override
+  public Icon getIcon(int flags) {
+    return PlatformIcons.VARIABLE_ICON;
+  }
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/BuildElementGenerator.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/BuildElementGenerator.java
new file mode 100644
index 0000000..ce39f11
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/BuildElementGenerator.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.psi.util;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildToken;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.Expression;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileFactory;
+import com.intellij.psi.impl.PsiFileFactoryImpl;
+import com.intellij.testFramework.LightVirtualFile;
+
+/**
+ * Creates dummy BuildElements, e.g. for renaming purposes.
+ */
+public class BuildElementGenerator {
+
+  public static BuildElementGenerator getInstance(Project project) {
+    return ServiceManager.getService(project, BuildElementGenerator.class);
+  }
+
+  private static final String DUMMY_FILENAME = "dummy.bzl";
+
+  private final Project project;
+
+  public BuildElementGenerator(Project project) {
+    this.project = project;
+  }
+
+  public PsiFile createDummyFile(String contents) {
+    PsiFileFactory factory = PsiFileFactory.getInstance(project);
+    LightVirtualFile virtualFile = new LightVirtualFile(DUMMY_FILENAME, BuildFileType.INSTANCE, contents);
+    PsiFile psiFile = ((PsiFileFactoryImpl) factory).trySetupPsiForFile(virtualFile, BuildFileLanguage.INSTANCE, false, true);
+    assert psiFile != null;
+    return psiFile;
+  }
+
+  public ASTNode createNameIdentifier(String name) {
+    PsiFile dummyFile = createDummyFile(name);
+    ASTNode referenceNode = dummyFile.getNode().getFirstChildNode();
+    ASTNode nameNode = referenceNode.getFirstChildNode();
+    if (nameNode.getElementType() != BuildToken.IDENTIFIER) {
+      throw new RuntimeException("Expecting an IDENTIFIER node directly below the BuildFile PSI element");
+    }
+    return nameNode;
+  }
+
+  public ASTNode createStringNode(String contents) {
+    PsiFile dummyFile = createDummyFile('"' + contents + '"');
+    ASTNode literalNode = dummyFile.getNode().getFirstChildNode();
+    ASTNode stringNode = literalNode.getFirstChildNode();
+    assert(stringNode.getElementType() == BuildToken.fromKind(TokenKind.STRING));
+    return stringNode;
+  }
+
+  public Argument.Keyword createKeywordArgument(String keyword, String value) {
+    String dummyText = String.format("foo(%s = \"%s\")", keyword, value);
+    FuncallExpression funcall = (FuncallExpression) createExpressionFromText(dummyText);
+    return (Argument.Keyword) funcall.getArguments()[0];
+  }
+
+  public Expression createExpressionFromText(String text) {
+    PsiFile dummyFile = createDummyFile(text);
+    PsiElement element = dummyFile.getFirstChild();
+    if (element instanceof Expression) {
+      return (Expression) element;
+    }
+    throw new RuntimeException("Could not parse text as expression: '" + text + "'");
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/PsiUtils.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/PsiUtils.java
new file mode 100644
index 0000000..e27794f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/psi/util/PsiUtils.java
@@ -0,0 +1,190 @@
+/*
+ * 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.psi.util;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.psi.AssignmentStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.Expression;
+import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.TargetExpression;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.CommonProcessors;
+import com.intellij.util.Processor;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Utility methods for working with PSI elements
+ */
+public class PsiUtils {
+
+  public static ASTNode createNewName(Project project, String name) {
+    return BuildElementGenerator.getInstance(project).createNameIdentifier(name);
+  }
+
+  public static ASTNode createNewLabel(Project project, String labelString) {
+    return BuildElementGenerator.getInstance(project).createStringNode(labelString);
+  }
+
+  @Nullable
+  public static PsiElement getPreviousNodeInTree(PsiElement element) {
+    PsiElement prevSibling = null;
+    while (element != null && (prevSibling = element.getPrevSibling()) == null) {
+      element = element.getParent();
+    }
+    return prevSibling != null ? lastElementInSubtree(prevSibling) : null;
+  }
+
+  /**
+   * The last element in the tree rooted at the given element.
+   */
+  public static PsiElement lastElementInSubtree(PsiElement element) {
+    PsiElement lastChild;
+    while ((lastChild = element.getLastChild()) != null) {
+      element = lastChild;
+    }
+    return element;
+  }
+
+  /**
+   * Walks up PSI tree, looking for a parent of the specified class. Stops searching when it reaches a
+   * parent of type PsiDirectory.
+   */
+  @Nullable
+  public static <T extends PsiElement> T getParentOfType(PsiElement element, Class<T> psiClass) {
+    PsiElement parent = element.getParent();
+    while (parent != null && !(parent instanceof PsiDirectory)) {
+      if (psiClass.isInstance(parent)) {
+        return (T) parent;
+      }
+      parent = parent.getParent();
+    }
+    return null;
+  }
+
+  @Nullable
+  public static <T extends PsiElement> T findFirstChildOfClassRecursive(PsiElement parent, Class<T> psiClass) {
+    List<T> holder = Lists.newArrayListWithExpectedSize(1);
+    Processor<T> getFirst = t -> {
+      holder.add(t);
+      return false;
+    };
+    processChildrenOfType(parent, getFirst, psiClass);
+    return holder.isEmpty() ? null : holder.get(0);
+  }
+
+  @Nullable
+  public static <T extends PsiElement> T findLastChildOfClassRecursive(PsiElement parent, Class<T> psiClass) {
+    List<T> holder = Lists.newArrayListWithExpectedSize(1);
+    Processor<T> getFirst = t -> {
+      holder.add(t);
+      return false;
+    };
+    processChildrenOfType(parent, getFirst, psiClass, true);
+    return holder.isEmpty() ? null : holder.get(0);
+  }
+
+  public static <T extends PsiElement> List<T> findAllChildrenOfClassRecursive(PsiElement parent, Class<T> psiClass) {
+    List<T> result = Lists.newArrayList();
+    processChildrenOfType(parent, new CommonProcessors.CollectProcessor(result), psiClass);
+    return result;
+  }
+
+  /**
+   * Walk through entire PSI tree rooted at 'element', processing all children of the given type.
+   * @return true if processing was stopped by the processor
+   */
+  public static <T extends PsiElement> boolean processChildrenOfType(
+    PsiElement element,
+    Processor<T> processor,
+    Class<T> psiClass) {
+    return processChildrenOfType(element, processor, psiClass, false);
+  }
+
+  /**
+   * Walk through entire PSI tree rooted at 'element', processing all children of the given type.
+   * @return true if processing was stopped by the processor
+   */
+  private static <T extends PsiElement> boolean processChildrenOfType(
+    PsiElement element,
+    Processor<T> processor,
+    Class<T> psiClass,
+    boolean reverseOrder) {
+    PsiElement child = reverseOrder ? element.getLastChild() : element.getFirstChild();
+    while (child != null) {
+      if (psiClass.isInstance(child)) {
+        if (!processor.process((T) child)) {
+          return true;
+        }
+      }
+      if (processChildrenOfType(child, processor, psiClass, reverseOrder)) {
+        return true;
+      }
+      child = reverseOrder ? child.getPrevSibling() : child.getNextSibling();
+    }
+    return false;
+  }
+
+  public static TextRange childRangeInParent(TextRange parentRange, TextRange childRange) {
+    return childRange.shiftRight(-parentRange.getStartOffset());
+  }
+
+  @Nullable
+  public static String getFilePath(@Nullable PsiFile file) {
+    VirtualFile virtualFile = file != null ? file.getVirtualFile() : null;
+    return virtualFile != null ? virtualFile.getPath() : null;
+  }
+
+  /**
+   * For ReferenceExpressions, follows the chain of references until it hits a non-ReferenceExpression.
+   * For other types, returns the input expression.
+   */
+  public static PsiElement getReferencedTarget(Expression expr) {
+    PsiElement element = expr;
+    while (element instanceof ReferenceExpression) {
+      PsiElement referencedElement = ((ReferenceExpression) element).getReferencedElement();
+      if (referencedElement == null) {
+        return element;
+      }
+      element = referencedElement;
+    }
+    return element;
+  }
+
+  /**
+   * For ReferenceExpressions, follows the chain of references until it hits a non-ReferenceExpression, then
+   * evaluates the value of that target.
+   * For other types, returns the input expression.
+   */
+  public static PsiElement getReferencedTargetValue(Expression expr) {
+    PsiElement element = getReferencedTarget(expr);
+    if (element instanceof TargetExpression) {
+      PsiElement parent = element.getParent();
+      if (parent instanceof AssignmentStatement) {
+        return ((AssignmentStatement) parent).getAssignedValue();
+      }
+    }
+    return element;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildNamesValidator.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildNamesValidator.java
new file mode 100644
index 0000000..595fe39
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildNamesValidator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.refactor;
+
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexer;
+import com.google.idea.blaze.base.lang.buildfile.lexer.BuildLexerBase;
+import com.google.idea.blaze.base.lang.buildfile.lexer.TokenKind;
+import com.intellij.lang.refactoring.NamesValidator;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Used for rename validation
+ */
+public class BuildNamesValidator implements NamesValidator {
+
+  @Override
+  public boolean isKeyword(String s, Project project) {
+    return false;
+  }
+
+  @Override
+  public boolean isIdentifier(String s, Project project) {
+    BuildLexer lexer = new BuildLexer(BuildLexerBase.LexerMode.Parsing);
+    lexer.start(s);
+    return lexer.getTokenEnd() == s.length() && lexer.getTokenKind() == TokenKind.IDENTIFIER;
+  }
+
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildRefactoringSupportProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildRefactoringSupportProvider.java
new file mode 100644
index 0000000..352252e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/refactor/BuildRefactoringSupportProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.refactor;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.TargetExpression;
+import com.intellij.lang.refactoring.RefactoringSupportProvider;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Supports 'safe delete'
+ */
+public class BuildRefactoringSupportProvider extends RefactoringSupportProvider {
+
+  @Override
+  public boolean isSafeDeleteAvailable(@NotNull PsiElement element) {
+    // basically a promise that 'find usages' works for this element
+    return element instanceof FunctionStatement
+      || element instanceof TargetExpression
+      || element instanceof FuncallExpression;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/ArgumentReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/ArgumentReference.java
new file mode 100644
index 0000000..e3b4f13
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/ArgumentReference.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.completion.NamedBuildLookupElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.psi.util.PsiTreeUtil;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Only keyword arguments resolve, but we include this class for code completion purposes.
+ * As the user is typing a keyword arg, they'll start with a positional arg element.
+ */
+public class ArgumentReference<T extends Argument> extends PsiReferenceBase<T> {
+
+  public ArgumentReference(T element, TextRange rangeInElement, boolean soft) {
+    super(element, rangeInElement, false);
+  }
+
+  @Nullable
+  protected FunctionStatement resolveFunction() {
+    FuncallExpression call = PsiTreeUtil.getParentOfType(myElement, FuncallExpression.class);
+    if (call == null) {
+      return null;
+    }
+    PsiElement callee = call.getReferencedElement();
+    return callee instanceof FunctionStatement ? (FunctionStatement) callee : null;
+  }
+
+  @Nullable
+  @Override
+  public PsiElement resolve() {
+    return null;
+  }
+
+  @Override
+  public Object[] getVariants() {
+    FunctionStatement function = resolveFunction();
+    if (function == null) {
+      return EMPTY_ARRAY;
+    }
+    List<LookupElement> params = Lists.newArrayList();
+    for (Parameter param : function.getParameters()) {
+      params.add(new NamedBuildLookupElement(param, QuoteType.NoQuotes));
+    }
+    return params.toArray();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java
new file mode 100644
index 0000000..7ee9a70
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/BuildReferenceManager.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.lang.buildfile.completion.BuildLookupElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.RuleName;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverProvider;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileSystem;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileSystemItem;
+import com.intellij.psi.PsiManager;
+import com.intellij.util.PathUtil;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.List;
+
+/**
+ * Handles reference caching and resolving labels to PSI elements.
+ */
+public class BuildReferenceManager {
+
+  public static BuildReferenceManager getInstance(Project project) {
+    return ServiceManager.getService(project, BuildReferenceManager.class);
+  }
+
+  private final Project project;
+
+  public BuildReferenceManager(Project project) {
+    this.project = project;
+  }
+
+  /**
+   * Finds the PSI element associated with the given label.
+   */
+  @Nullable
+  public PsiElement resolveLabel(Label label) {
+    return resolveLabel(label.blazePackage(), label.ruleName(), false);
+  }
+
+  /**
+   * Finds the PSI element associated with the given label.
+   */
+  @Nullable
+  public PsiElement resolveLabel(WorkspacePath packagePath, RuleName ruleName, boolean excludeRules) {
+    File packageDir = resolvePackage(packagePath);
+    if (packageDir == null) {
+      return null;
+    }
+
+    if (!excludeRules) {
+      FuncallExpression target = findRule(packageDir, ruleName);
+      if (target != null) {
+        return target;
+      }
+    }
+
+    // try a direct file reference (e.g. ":a.java")
+    File fullFile = new File(packageDir, ruleName.toString());
+    if (FileAttributeProvider.getInstance().exists(fullFile)) {
+      return resolveFile(fullFile);
+    }
+
+    return null;
+  }
+
+  private FuncallExpression findRule(File packageDir, RuleName ruleName) {
+    BuildFile psiFile = findBuildFile(packageDir);
+    return psiFile != null ? psiFile.findRule(ruleName.toString()) : null;
+  }
+
+  @Nullable
+  public PsiFileSystemItem resolveFile(File file) {
+    VirtualFile vf = getFileSystem().findFileByPath(file.getPath());
+    if (vf == null) {
+      return null;
+    }
+    PsiManager manager = PsiManager.getInstance(project);
+    return vf.isDirectory() ? manager.findDirectory(vf) : manager.findFile(vf);
+  }
+
+  @Nullable
+  public File resolvePackage(@Nullable WorkspacePath packagePath) {
+    return resolveWorkspaceRelativePath(packagePath != null ? packagePath.relativePath() : null);
+  }
+
+  @Nullable
+  private File resolveWorkspaceRelativePath(@Nullable String relativePath) {
+    WorkspacePathResolver pathResolver = getWorkspacePathResolver();
+    if (pathResolver == null || relativePath == null) {
+      return null;
+    }
+    return pathResolver.resolveToFile(relativePath);
+  }
+
+  @Nullable
+  private WorkspacePathResolver getWorkspacePathResolver() {
+    return WorkspacePathResolverProvider.getInstance(project).getPathResolver();
+  }
+
+  /**
+   * Finds all child directories. If exactly one is found, continue traversing (and appending to LookupElement string)
+   * until there are multiple options.<br>
+   * Used for package path completion suggestions.
+   */
+  public BuildLookupElement[] resolvePackageLookupElements(FileLookupData lookupData) {
+    String relativePath = lookupData.filePathFragment;
+    File file = resolveWorkspaceRelativePath(relativePath);
+
+    FileAttributeProvider provider = FileAttributeProvider.getInstance();
+    String pathFragment = "";
+    if (file == null || (!provider.isDirectory(file) && !relativePath.endsWith("/"))) {
+      // we might be partway through a file name. Try the parent directory
+      relativePath = PathUtil.getParentPath(relativePath);
+      file = resolveWorkspaceRelativePath(relativePath);
+      pathFragment = StringUtil.trimStart(lookupData.filePathFragment.substring(relativePath.length()), "/");
+    }
+    if (file == null || !provider.isDirectory(file)) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    VirtualFile vf = getFileSystem().findFileByPath(file.getPath());
+    if (vf == null || !vf.isDirectory()) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    BuildLookupElement[] uniqueLookup = new BuildLookupElement[1];
+    while (true) {
+      VirtualFile[] children = vf.getChildren();
+      if (children == null || children.length == 0) {
+        return uniqueLookup[0] != null ? uniqueLookup : BuildLookupElement.EMPTY_ARRAY;
+      }
+      List<VirtualFile> validChildren = Lists.newArrayListWithCapacity(children.length);
+      for (VirtualFile child : children) {
+        if (child.getName().startsWith(pathFragment) && lookupData.acceptFile(child)) {
+          validChildren.add(child);
+        }
+      }
+      if (validChildren.isEmpty()) {
+        return uniqueLookup[0] != null ? uniqueLookup : BuildLookupElement.EMPTY_ARRAY;
+      }
+      if (validChildren.size() > 1) {
+        return uniqueLookup[0] != null ?
+               uniqueLookup : lookupsForFiles(validChildren, lookupData);
+      }
+      // continue traversing while there's only one option
+      uniqueLookup[0] = lookupForFile(validChildren.get(0), lookupData);
+      pathFragment = "";
+      vf = validChildren.get(0);
+    }
+  }
+
+  private BuildLookupElement[] lookupsForFiles(List<VirtualFile> files, FileLookupData lookupData) {
+    BuildLookupElement[] lookups = new BuildLookupElement[files.size()];
+    for (int i = 0; i < files.size(); i++) {
+      lookups[i] = lookupForFile(files.get(i), lookupData);
+    }
+    return lookups;
+  }
+
+  private BuildLookupElement lookupForFile(VirtualFile file, FileLookupData lookupData) {
+    WorkspacePath workspacePath = getWorkspaceRelativePath(file.getPath());
+    return lookupData.lookupElementForFile(project, file, workspacePath);
+  }
+
+  @Nullable
+  public BuildFile resolveBlazePackage(String workspaceRelativePath) {
+    workspaceRelativePath = StringUtil.trimStart(workspaceRelativePath, "//");
+    return resolveBlazePackage(WorkspacePath.createIfValid(workspaceRelativePath));
+  }
+
+  @Nullable
+  public BuildFile resolveBlazePackage(@Nullable WorkspacePath path) {
+    return findBuildFile(resolvePackage(path));
+  }
+
+  @Nullable
+  private BuildFile findBuildFile(@Nullable File packageDirectory) {
+    FileAttributeProvider provider = FileAttributeProvider.getInstance();
+    if (packageDirectory == null || !provider.isDirectory(packageDirectory)) {
+      return null;
+    }
+    File buildFile = new File(packageDirectory, "BUILD");
+    if (!provider.exists(buildFile)) {
+      return null;
+    }
+    VirtualFile vf = getFileSystem().findFileByPath(buildFile.getPath());
+    if (vf == null) {
+      return null;
+    }
+    PsiFile psiFile = PsiManager.getInstance(project).findFile(vf);
+    return psiFile instanceof BuildFile ? (BuildFile) psiFile : null;
+  }
+
+  /**
+   * For files references, returns the parent directory.<br>
+   * For rule references, return the blaze package directory.
+   */
+  @Nullable
+  public File resolveParentDirectory(@Nullable Label label) {
+    return label != null ? resolveParentDirectory(label.blazePackage(), label.ruleName()) : null;
+  }
+
+  @Nullable
+  private File resolveParentDirectory(WorkspacePath packagePath, RuleName ruleName) {
+    File packageFile = resolvePackage(packagePath);
+    if (packageFile == null) {
+      return null;
+    }
+    String rulePathParent = PathUtil.getParentPath(ruleName.toString());
+    return new File(packageFile, rulePathParent);
+  }
+
+  @Nullable
+  public WorkspacePath getWorkspaceRelativePath(String absolutePath) {
+    WorkspacePathResolver pathResolver = getWorkspacePathResolver();
+    WorkspaceRoot workspaceRoot = pathResolver != null ? pathResolver.getWorkspaceRoot() : null;
+    return workspaceRoot != null ? getWorkspaceRelativePath(workspaceRoot, absolutePath) : null;
+  }
+
+  @Nullable
+  static private WorkspacePath getWorkspaceRelativePath(WorkspaceRoot workspaceRoot, String absolutePath) {
+    File file = new File(absolutePath);
+    if (workspaceRoot.isInWorkspace(file)) {
+      return workspaceRoot.workspacePathFor(file);
+    }
+    return null;
+  }
+
+  private static VirtualFileSystem getFileSystem() {
+    if (ApplicationManager.getApplication().isUnitTestMode()) {
+      return TempFileSystem.getInstance();
+    }
+    return LocalFileSystem.getInstance();
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
new file mode 100644
index 0000000..8f2ac8a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/FileLookupData.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.completion.FilePathLookupElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.NullableLazyValue;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.VirtualFileFilter;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.util.PathUtil;
+import com.intellij.util.PlatformIcons;
+import icons.BlazeIcons;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+
+/**
+ * The data relevant to finding file lookups.
+ */
+public class FileLookupData {
+
+  public enum PathFormat {
+    /** BUILD label without a leading '//', which can only reference targets in the same package. */
+    PackageLocal,
+    /** a BUILD label with leading '//', which can reference targets in other packages. */
+    NonLocal,
+    /** a path string which can reference any files, and has no leading '//'. */
+    NonLocalWithoutInitialBackslashes
+  }
+
+  @Nullable
+  public static FileLookupData nonLocalFileLookup(String originalLabel, StringLiteral element) {
+    return nonLocalFileLookup(originalLabel, element.getContainingFile(), element.getQuoteType(), PathFormat.NonLocal);
+  }
+
+  @Nullable
+  public static FileLookupData nonLocalFileLookup(String originalLabel,
+                                                  @Nullable BuildFile containingFile,
+                                                  QuoteType quoteType,
+                                                  PathFormat pathFormat) {
+    if (originalLabel.indexOf(':') != -1) {
+      // it's a package-local reference
+      return null;
+    }
+    // handle the single '/' case by calling twice.
+    String relativePath = StringUtil.trimStart(StringUtil.trimStart(originalLabel, "/"), "/");
+    if (relativePath.startsWith("/")) {
+      return null;
+    }
+    return new FileLookupData(originalLabel, containingFile, null, relativePath, pathFormat, quoteType, null);
+  }
+
+  @Nullable
+  public static FileLookupData packageLocalFileLookup(String originalLabel, StringLiteral element) {
+    if (originalLabel.startsWith("/")) {
+      return null;
+    }
+    BlazePackage blazePackage = element.getBlazePackage();
+    BuildFile baseBuildFile = blazePackage != null ? blazePackage.buildFile : null;
+    return packageLocalFileLookup(originalLabel, element, baseBuildFile, null);
+  }
+
+  @Nullable
+  public static FileLookupData packageLocalFileLookup(String originalLabel,
+                                                      StringLiteral element,
+                                                      @Nullable BuildFile basePackage,
+                                                      @Nullable VirtualFileFilter fileFilter) {
+    String basePackagePath = basePackage != null ? basePackage.getWorkspaceRelativePackagePath() : null;
+    if (basePackagePath == null) {
+      return null;
+    }
+    String filePath = basePackagePath + "/" + LabelUtils.getRuleComponent(originalLabel);
+    return new FileLookupData(originalLabel, basePackage, basePackagePath, filePath, PathFormat.PackageLocal, element.getQuoteType(), fileFilter);
+  }
+
+
+  private final String originalLabel;
+  private final BuildFile containingFile;
+  @Nullable
+  private final String containingPackage;
+  public final String filePathFragment;
+  public final PathFormat pathFormat;
+  private final QuoteType quoteType;
+  @Nullable
+  private final VirtualFileFilter fileFilter;
+
+  private FileLookupData(
+    String originalLabel,
+    @Nullable BuildFile containingFile,
+    @Nullable String containingPackage,
+    String filePathFragment,
+    PathFormat pathFormat,
+    QuoteType quoteType,
+    @Nullable VirtualFileFilter fileFilter) {
+
+    this.originalLabel = originalLabel;
+    this.containingFile = containingFile;
+    this.containingPackage = containingPackage;
+    this.fileFilter = fileFilter;
+    this.filePathFragment = filePathFragment;
+    this.pathFormat = pathFormat;
+    this.quoteType = quoteType;
+
+    assert(pathFormat != PathFormat.PackageLocal || (containingPackage != null && containingFile != null));
+  }
+
+  public boolean acceptFile(VirtualFile file) {
+    if (fileFilter != null && !fileFilter.accept(file)) {
+      return false;
+    }
+    if (pathFormat != PathFormat.PackageLocal) {
+      return file.isDirectory();
+    }
+    if (file.equals(containingFile.getOriginalFile().getVirtualFile())) {
+      return false;
+    }
+    boolean blazePackage = file.findChild("BUILD") != null;
+    return !blazePackage;
+  }
+
+  public FilePathLookupElement lookupElementForFile(Project project, VirtualFile file, @Nullable WorkspacePath workspacePath) {
+    NullableLazyValue<Icon> icon = new NullableLazyValue<Icon>() {
+      @Override
+      protected Icon compute() {
+        if (file.findChild("BUILD") != null) {
+          return BlazeIcons.BuildFile;
+        }
+        if (file.isDirectory()) {
+          return PlatformIcons.FOLDER_ICON;
+        }
+        PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
+        return psiFile != null ? psiFile.getIcon(0) : AllIcons.FileTypes.Any_type;
+      }
+    };
+    String fullLabel = workspacePath != null ? getFullLabel(workspacePath.relativePath()) : file.getPath();
+    String itemText = workspacePath != null ? getItemText(workspacePath.relativePath()) : fullLabel;
+    return new FilePathLookupElement(fullLabel, itemText, quoteType, icon);
+  }
+
+  private String getFullLabel(String relativePath) {
+    if (pathFormat != PathFormat.PackageLocal) {
+      if (pathFormat == PathFormat.NonLocal) {
+        relativePath = "//" + relativePath;
+      }
+      return relativePath;
+    }
+    String prefix;
+    int colonIndex = originalLabel.indexOf(':');
+    if (originalLabel.startsWith("/")) {
+      prefix = colonIndex == -1 ? originalLabel + ":" : originalLabel.substring(0, colonIndex + 1);
+    } else {
+      prefix = originalLabel.substring(0, colonIndex + 1);
+    }
+    return prefix + getItemText(relativePath);
+  }
+
+  private String getItemText(String relativePath) {
+    if (pathFormat == PathFormat.PackageLocal) {
+      return StringUtil.trimStart(relativePath.substring(containingPackage.length()), "/");
+    }
+    String parentPath = PathUtil.getParentPath(relativePath);
+    while (!parentPath.isEmpty()) {
+      if (filePathFragment.startsWith(parentPath + "/")) {
+        return StringUtil.trimStart(relativePath, parentPath + "/");
+      } else if (filePathFragment.startsWith(parentPath)) {
+        return StringUtil.trimStart(relativePath, parentPath);
+      }
+      parentPath = PathUtil.getParentPath(parentPath);
+    }
+    return relativePath;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java
new file mode 100644
index 0000000..a028a05
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/FuncallReference.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.util.IncorrectOperationException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Reference from a function call to the function declaration
+ */
+public class FuncallReference extends PsiReferenceBase<FuncallExpression> {
+
+  public FuncallReference(FuncallExpression element, TextRange rangeInElement) {
+    super(element, rangeInElement, /*soft*/ true);
+  }
+
+  @Nullable
+  @Override
+  public FunctionStatement resolve() {
+    String functionName = myElement.getFunctionName();
+    BuildFile file = (BuildFile) myElement.getContainingFile();
+    if (functionName == null || file == null) {
+      return null;
+    }
+    return file.findFunctionInScope(functionName);
+  }
+
+  @Override
+  public Object[] getVariants() {
+    return EMPTY_ARRAY;
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    ASTNode oldNode = myElement.getFunctionNameNode();
+    if (oldNode != null) {
+      ASTNode newNode = PsiUtils.createNewName(myElement.getProject(), newElementName);
+      myElement.getNode().replaceChild(oldNode, newNode);
+    }
+    return myElement;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/GlobReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/GlobReference.java
new file mode 100644
index 0000000..3dfbc81
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/GlobReference.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.lang.buildfile.globbing.UnixGlob;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.source.resolve.reference.impl.PsiPolyVariantCachingReference;
+import com.intellij.util.IncorrectOperationException;
+
+import java.io.File;
+import java.util.List;
+import java.util.function.Predicate;
+
+/**
+ * References from a glob to a list of files contained in the same blaze package.
+ */
+public class GlobReference extends PsiPolyVariantCachingReference {
+
+  private static final Logger LOG = Logger.getInstance(GlobReference.class);
+
+  private final GlobExpression element;
+
+  public GlobReference(GlobExpression element) {
+    this.element = element;
+  }
+
+  /**
+   * Returns true iff the complete, resolved glob references the specified file.<p>
+   * In particular, it's not concerned with individual patterns referencing
+   * the file, only whether the overall glob does
+   * (i.e. returns false if the file is explicitly excluded).
+   */
+  public boolean matches(String packageRelativePath, boolean isDirectory) {
+    if (isDirectory && element.areDirectoriesExcluded()) {
+      return false;
+    }
+    for (String exclude : resolveListContents(element.getExcludes())) {
+      if (UnixGlob.matches(exclude, packageRelativePath)) {
+        return false;
+      }
+    }
+    for (String include : resolveListContents(element.getIncludes())) {
+      if (UnixGlob.matches(include, packageRelativePath)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns true iff an include pattern *without wildcards* matches the given path and
+   * it's not excluded.
+   */
+  public boolean matchesDirectly(String packageRelativePath, boolean isDirectory) {
+    if (isDirectory && element.areDirectoriesExcluded()) {
+      return false;
+    }
+    for (String exclude : resolveListContents(element.getExcludes())) {
+      if (UnixGlob.matches(exclude, packageRelativePath)) {
+        return false;
+      }
+    }
+    for (String include : resolveListContents(element.getIncludes())) {
+      if (!hasWildcard(include) && UnixGlob.matches(include, packageRelativePath)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean hasWildcard(String pattern) {
+    return pattern.contains("*");
+  }
+
+  @Override
+  protected ResolveResult[] resolveInner(boolean incompleteCode, PsiFile containingFile) {
+    File containingDirectory = ((BuildFile) containingFile).getFile().getParentFile();
+    if (containingDirectory == null) {
+      return ResolveResult.EMPTY_ARRAY;
+    }
+    List<String> includes = resolveListContents(element.getIncludes());
+    List<String> excludes = resolveListContents(element.getExcludes());
+    boolean directoriesExcluded = element.areDirectoriesExcluded();
+    if (includes.isEmpty()) {
+      return ResolveResult.EMPTY_ARRAY;
+    }
+
+    try {
+      List<File> files = UnixGlob.forPath(containingDirectory)
+        .addPatterns(includes)
+        .addExcludes(excludes)
+        .setExcludeDirectories(directoriesExcluded)
+        .setDirectoryFilter(directoryFilter(containingDirectory.getPath()))
+        .glob();
+      List<ResolveResult> results = Lists.newArrayListWithCapacity(files.size());
+      for (File file : files) {
+        PsiFileSystemItem psiFile = BuildReferenceManager.getInstance(element.getProject()).resolveFile(file);
+        if (psiFile != null) {
+          results.add(new PsiElementResolveResult(psiFile));
+        }
+      }
+      return results.toArray(ResolveResult.EMPTY_ARRAY);
+
+    } catch (Exception e) {
+      return ResolveResult.EMPTY_ARRAY;
+    }
+  }
+
+  private static Predicate<File> directoryFilter(String base) {
+    return file -> {
+      if (base.equals(file.getPath())) {
+        return true;
+      }
+      File child = new File(file, "BUILD");
+      FileAttributeProvider attributeProvider = FileAttributeProvider.getInstance();
+      return !attributeProvider.exists(child) || attributeProvider.isDirectory(child);
+    };
+  }
+
+  private static List<String> resolveListContents(Expression expr) {
+    if (expr == null) {
+      return ImmutableList.of();
+    }
+    PsiElement rootElement = PsiUtils.getReferencedTargetValue(expr);
+    if (!(rootElement instanceof ListLiteral)) {
+      return ImmutableList.of();
+    }
+    Expression[] children = ((ListLiteral) rootElement).getElements();
+    List<String> strings = Lists.newArrayListWithCapacity(children.length);
+    for (Expression child : children) {
+      if (child instanceof StringLiteral) {
+        strings.add(((StringLiteral) child).getStringContents());
+      }
+    }
+    return strings;
+  }
+
+  @Override
+  public GlobExpression getElement() {
+    return element;
+  }
+
+  @Override
+  public TextRange getRangeInElement() {
+    return element.getReferenceTextRange();
+  }
+
+  @Override
+  public boolean isSoft() {
+    return true;
+  }
+
+  @Override
+  public Object[] getVariants() {
+    return EMPTY_ARRAY;
+  }
+
+  @Override
+  public String getCanonicalText() {
+    return getValue();
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    return element;
+  }
+
+  @Override
+  public PsiElement bindToElement(PsiElement element) throws IncorrectOperationException {
+    return this.element;
+  }
+
+  public String getValue() {
+    String text = element.getText();
+    final TextRange range = getRangeInElement();
+    try {
+      return range.substring(text);
+    }
+    catch (StringIndexOutOfBoundsException e) {
+      LOG.error("Wrong range in reference " + this + ": " + range + ". Reference text: '" + text + "'", e);
+      return text;
+    }
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/KeywordArgumentReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/KeywordArgumentReference.java
new file mode 100644
index 0000000..da59a2f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/KeywordArgumentReference.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.Parameter;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.util.IncorrectOperationException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Reference from keyword argument to a named function parameter.
+ * TODO: This is soft, because we can't always find the function. However we should implement error highlighting
+ * for imported Skylark functions.
+ */
+public class KeywordArgumentReference extends ArgumentReference<Argument.Keyword> {
+
+  public KeywordArgumentReference(Argument.Keyword element, TextRange rangeInElement) {
+    super(element, rangeInElement, false);
+  }
+
+  /**
+   * Find the referenced function. If it has a keyword parameter with matching name,
+   * return that. Otherwise if it has a **kwargs param, return that. Else return the
+   * function itself.
+   */
+  @Nullable
+  @Override
+  public PsiElement resolve() {
+    String keyword = myElement.getName();
+    if (keyword == null) {
+      return null;
+    }
+    FunctionStatement function = resolveFunction();
+    if (function == null) {
+      return null;
+    }
+    Parameter.StarStar kwargsParameter = null;
+    for (Parameter param : function.getParameters()) {
+      if (param instanceof Parameter.StarStar) {
+        kwargsParameter = (Parameter.StarStar) param;
+        continue;
+      }
+      if (keyword.equals(param.getName())) {
+        return param;
+      }
+    }
+    if (kwargsParameter != null) {
+      return kwargsParameter;
+    }
+    return null;
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    ASTNode oldNode = myElement.getNameNode();
+    if (oldNode != null) {
+      ASTNode newNode = PsiUtils.createNewName(myElement.getProject(), newElementName);
+      myElement.getNode().replaceChild(oldNode, newNode);
+    }
+    return myElement;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java
new file mode 100644
index 0000000..41870e7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelReference.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.completion.BuildLookupElement;
+import com.google.idea.blaze.base.lang.buildfile.completion.LabelRuleLookupElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile.BlazeFileType;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.lang.buildfile.search.ResolveUtil;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.vfs.VirtualFileFilter;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Converts a blaze label into an absolute path, then resolves that path to a PsiElements
+ */
+public class LabelReference extends PsiReferenceBase<StringLiteral> {
+
+  public LabelReference(StringLiteral element, boolean soft) {
+    super(element, new TextRange(0, element.getTextLength()), soft);
+  }
+
+  @Nullable
+  @Override
+  public PsiElement resolve() {
+    /* Possibilities:
+     * - target
+     * - data file (.java, .txt, etc.)
+     * - glob contents (not yet handling globs)
+     */
+    return resolveTarget(myElement.getStringContents());
+  }
+
+  @Nullable
+  private PsiElement resolveTarget(String labelString) {
+    Label label = getLabel(labelString);
+    if (label == null) {
+      return null;
+    }
+    if (!validLabelLocation(myElement)) {
+      return null;
+    }
+    if (!labelString.startsWith("//") && insideSkylarkExtension(myElement)) {
+      return getReferenceManager().resolveLabel(label.blazePackage(), label.ruleName(), true);
+    }
+    return getReferenceManager().resolveLabel(label);
+  }
+
+  /**
+   * Hack: don't include 'name' keyword arguments -- they'll be a reference to the enclosing function call / rule,
+   * and show up as unnecessary references to that rule.
+   */
+  private static boolean validLabelLocation(StringLiteral element) {
+    PsiElement parent = element.getParent();
+    if (parent instanceof Argument.Keyword) {
+      String argName = ((Argument.Keyword) parent).getName();
+      if ("name".equals(argName)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @NotNull
+  @Override
+  public Object[] getVariants() {
+    if (!validLabelLocation(myElement)) {
+      return EMPTY_ARRAY;
+    }
+    String labelString = LabelUtils.trimToDummyIdentifier(myElement.getStringContents());
+    return ArrayUtil.mergeArrays(
+      getRuleLookups(labelString),
+      getFileLookups(labelString));
+  }
+
+  private BuildLookupElement[] getRuleLookups(String labelString) {
+    if (labelString.endsWith("/")
+        || (labelString.startsWith("/") && !labelString.contains(":"))
+        || skylarkExtensionReference(myElement)) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    String packagePrefix = LabelUtils.getPackagePathComponent(labelString);
+    BuildFile referencedBuildFile = LabelUtils.getReferencedBuildFile(myElement.getContainingFile(), packagePrefix);
+    if (referencedBuildFile == null) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    String self = null;
+    if (referencedBuildFile == myElement.getContainingFile()) {
+      FuncallExpression funcall = PsiUtils.getParentOfType(myElement, FuncallExpression.class);
+      if (funcall != null) {
+        self = funcall.getName();
+      }
+    }
+    return LabelRuleLookupElement.collectAllRules(referencedBuildFile, labelString, packagePrefix, self, myElement.getQuoteType());
+  }
+
+  private BuildLookupElement[] getFileLookups(String labelString) {
+    if (labelString.startsWith("//") || labelString.equals("/")) {
+      return getNonLocalFileLookups(labelString);
+    }
+    return getPackageLocalFileLookups(labelString);
+  }
+
+  private BuildLookupElement[] getNonLocalFileLookups(String labelString) {
+    BuildLookupElement[] skylarkExtLookups = getSkylarkExtensionLookups(labelString);
+    FileLookupData lookupData = FileLookupData.nonLocalFileLookup(labelString, myElement);
+    BuildLookupElement[] packageLookups = lookupData != null
+                                          ? getReferenceManager().resolvePackageLookupElements(lookupData)
+                                          : BuildLookupElement.EMPTY_ARRAY;
+    return ArrayUtil.mergeArrays(skylarkExtLookups, packageLookups);
+  }
+
+  private BuildLookupElement[] getPackageLocalFileLookups(String labelString) {
+    if (skylarkExtensionReference(myElement)) {
+      return getSkylarkExtensionLookups(labelString);
+    }
+    FileLookupData lookupData = FileLookupData.packageLocalFileLookup(labelString, myElement);
+    return lookupData != null
+           ? getReferenceManager().resolvePackageLookupElements(lookupData)
+           : BuildLookupElement.EMPTY_ARRAY;
+  }
+
+  private BuildLookupElement[] getSkylarkExtensionLookups(String labelString) {
+    if (!skylarkExtensionReference(myElement)) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    String packagePrefix = LabelUtils.getPackagePathComponent(labelString);
+    BuildFile parentFile = myElement.getContainingFile();
+    if (parentFile == null) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    BlazePackage containingPackage = BlazePackage.getContainingPackage(parentFile);
+    if (containingPackage == null) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    BuildFile referencedBuildFile = LabelUtils.getReferencedBuildFile(containingPackage.buildFile, packagePrefix);
+    VirtualFileFilter filter = file ->
+      file.isDirectory() || ("bzl".equals(file.getExtension()) && !file.getPath().equals(parentFile.getFilePath()));
+    FileLookupData lookupData = FileLookupData.packageLocalFileLookup(labelString, myElement, referencedBuildFile, filter);
+
+    return lookupData != null
+           ? getReferenceManager().resolvePackageLookupElements(lookupData)
+           : BuildLookupElement.EMPTY_ARRAY;
+  }
+
+  private BuildReferenceManager getReferenceManager() {
+    return BuildReferenceManager.getInstance(myElement.getProject());
+  }
+
+  @Override
+  public PsiElement bindToElement(PsiElement element) throws IncorrectOperationException {
+    PsiFile file = ResolveUtil.asFileSearch(element);
+    if (file == null) {
+      return super.bindToElement(element);
+    }
+    if (file.equals(resolve())) {
+      return myElement;
+    }
+    BlazePackage currentPackageDir = myElement.getBlazePackage();
+    if (currentPackageDir == null) {
+      return myElement;
+    }
+    BlazePackage newPackageDir = BlazePackage.getContainingPackage(file);
+    if (!currentPackageDir.equals(newPackageDir)) {
+      return myElement;
+    }
+
+    String newRuleName = newPackageDir.getPackageRelativePath(file.getViewProvider().getVirtualFile().getPath());
+    return handleRename(newRuleName);
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    String currentString = myElement.getStringContents();
+    Label label = getLabel(currentString);
+    if (label == null) {
+      return myElement;
+    }
+    String ruleName = label.ruleName().toString();
+    String newRuleName = newElementName;
+
+    // handle subdirectories
+    int lastSlashIndex = ruleName.lastIndexOf('/');
+    if (lastSlashIndex != -1) {
+      newRuleName = ruleName.substring(0, lastSlashIndex + 1) + newElementName;
+    }
+
+    String packageString = LabelUtils.getPackagePathComponent(currentString);
+    if (packageString.isEmpty() && !currentString.contains(":")) {
+      return handleRename(newRuleName);
+    }
+    return handleRename(packageString + ":" + newRuleName);
+  }
+
+  private PsiElement handleRename(String newStringContents) {
+    ASTNode node = myElement.getNode();
+    node.replaceChild(node.getFirstChildNode(), PsiUtils.createNewLabel(myElement.getProject(), newStringContents));
+    return myElement;
+  }
+
+  @Nullable
+  private Label getLabel(String labelString) {
+    if (labelString.indexOf('*') != -1) {
+      // don't even try to handle globs, yet.
+      return null;
+    }
+    BlazePackage blazePackage = myElement.getBlazePackage();
+    return LabelUtils.createLabelFromString(blazePackage != null ? blazePackage.buildFile : null, labelString);
+  }
+
+  private static boolean skylarkExtensionReference(StringLiteral element) {
+    PsiElement parent = element.getParent();
+    if (!(parent instanceof LoadStatement)) {
+      return false;
+    }
+    return ((LoadStatement) parent).getImportPsiElement() == element;
+  }
+
+  private static boolean insideSkylarkExtension(StringLiteral element) {
+    BuildFile containingFile = element.getContainingFile();
+    return containingFile != null && containingFile.getBlazeFileType() == BlazeFileType.SkylarkExtension;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java
new file mode 100644
index 0000000..43d84b8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LabelUtils.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.RuleName;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.intellij.codeInsight.completion.CompletionUtilCore;
+import com.intellij.util.PathUtil;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Utility methods for working with blaze labels.
+ */
+public class LabelUtils {
+
+  /**
+   * Label referring to the given file, or null if it cannot be determined.
+   */
+  @Nullable
+  public static Label createLabelForFile(BlazePackage blazePackage, @Nullable String filePath) {
+    if (blazePackage == null || filePath == null) {
+      return null;
+    }
+    String relativeFilePath = blazePackage.getPackageRelativePath(filePath);
+    if (relativeFilePath == null) {
+      return null;
+    }
+    return createLabelFromRuleName(blazePackage, relativeFilePath);
+  }
+
+  /**
+   * Returns null if this is not a valid Label (if either the package path or rule name are invalid)
+   */
+  @Nullable
+  public static Label createLabelFromRuleName(@Nullable BlazePackage blazePackage, @Nullable String ruleName) {
+    if (blazePackage == null || ruleName == null) {
+      return null;
+    }
+    WorkspacePath packagePath = blazePackage.buildFile.getPackageWorkspacePath();
+    RuleName name = RuleName.createIfValid(ruleName);
+    if (packagePath == null || name == null) {
+      return null;
+    }
+    // TODO: Is Label too inefficient?
+    // (validation done twice,creating List during constructor, re-parsing to extract the package/rule each time)
+    return new Label(packagePath, name);
+  }
+
+  /**
+   * Canonicalizes the label (to the form //packagePath:packageRelativeTarget).
+   * Returns null if the string does not represent a valid label.
+   */
+  @Nullable
+  public static Label createLabelFromString(@Nullable BuildFile file, @Nullable String labelString) {
+    if (labelString == null) {
+      return null;
+    }
+    int colonIndex = labelString.indexOf(':');
+    if (labelString.startsWith("//")) {
+      if (colonIndex == -1) {
+        // add the implicit rule name
+        labelString += ":" + PathUtil.getFileName(labelString);
+      }
+      return Label.createIfValid(labelString);
+    }
+    WorkspacePath packagePath = file != null ? file.getPackageWorkspacePath() : null;
+    if (packagePath == null) {
+      return null;
+    }
+    String localPath = colonIndex == -1 ? labelString : labelString.substring(1);
+    return Label.createIfValid("//" + packagePath.relativePath() + ":" + localPath);
+  }
+
+  /**
+   * The blaze file referenced by the label.
+   */
+  @Nullable
+  public static BuildFile getReferencedBuildFile(@Nullable BuildFile containingFile, String packagePathComponent) {
+    if (containingFile == null) {
+      return null;
+    }
+    if (!packagePathComponent.startsWith("//")) {
+      return containingFile;
+    }
+    return BuildReferenceManager.getInstance(containingFile.getProject()).resolveBlazePackage(packagePathComponent);
+  }
+
+  public static String getRuleComponent(String labelString) {
+    if (labelString.startsWith("/")) {
+      int colonIndex = labelString.indexOf(':');
+      return colonIndex == -1 ? "" : labelString.substring(colonIndex + 1);
+    }
+    return labelString.startsWith(":") ? labelString.substring(1) : labelString;
+  }
+
+  public static String getPackagePathComponent(String labelString) {
+    if (!labelString.startsWith("//")) {
+      return "";
+    }
+    int colonIndex = labelString.indexOf(':');
+    return colonIndex == -1 ? labelString : labelString.substring(0, colonIndex);
+  }
+
+  /**
+   * 'load' reference. Of the form [path][/ or :][extra_path/]file_name.bzl
+   */
+  @Nullable
+  public static String getNiceSkylarkFileName(@Nullable String path) {
+    if (path == null) {
+      return null;
+    }
+    int colonIndex = path.lastIndexOf(":");
+    if (colonIndex != -1) {
+      path = path.substring(colonIndex + 1);
+    }
+    int lastSlash = path.lastIndexOf("/");
+    if (lastSlash == -1) {
+      return path;
+    }
+    return path.substring(lastSlash + 1);
+  }
+
+  /**
+   * All the possible strings which could resolve to the given target.
+   * @param includePackageLocalLabels if true, include strings omitting the package path
+   */
+  public static List<String> getAllValidLabelStrings(Label label, boolean includePackageLocalLabels) {
+    List<String> strings = Lists.newArrayList();
+    strings.add(label.toString());
+    String packagePath = label.blazePackage().relativePath();
+    if (packagePath.isEmpty()) {
+      return strings;
+    }
+    String ruleName = label.ruleName().toString();
+    if (PathUtil.getFileName(packagePath).equals(ruleName)) {
+      strings.add("//" + packagePath); // implicit rule name equal to package name
+    }
+    if (includePackageLocalLabels) {
+      strings.add(":" + ruleName);
+      strings.add(ruleName);
+    }
+    return strings;
+  }
+
+  /**
+   * IntelliJ inserts an identifier string at the caret position during code completion.<br>
+   * We're only interested in the portion of the string before the caret, so trim the rest.
+   */
+  public static String trimToDummyIdentifier(String string) {
+    int dummyIdentifierIndex = string.indexOf(CompletionUtilCore.DUMMY_IDENTIFIER);
+    if (dummyIdentifierIndex == -1) {
+      dummyIdentifierIndex = string.indexOf(CompletionUtilCore.DUMMY_IDENTIFIER_TRIMMED);
+    }
+    return dummyIdentifierIndex == -1 ? string : string.substring(0, dummyIdentifierIndex);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java
new file mode 100644
index 0000000..5359f94
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LoadedSymbolReference.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.completion.CompletionResultsProcessor;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.util.IncorrectOperationException;
+
+import javax.annotation.Nullable;
+
+/**
+ * References from load statement string to a function or variable in a Skylark extension
+ */
+public class LoadedSymbolReference extends PsiReferenceBase<StringLiteral> {
+
+  private final LabelReference bzlFileReference;
+
+  public LoadedSymbolReference(StringLiteral element, LabelReference bzlFileReference) {
+    super(element, new TextRange(0, element.getTextLength()), /*soft*/ false);
+    this.bzlFileReference = bzlFileReference;
+  }
+
+  @Nullable
+  @Override
+  public BuildElement resolve() {
+    PsiElement bzlFile = bzlFileReference.resolve();
+    if (!(bzlFile instanceof BuildFile)) {
+      return null;
+    }
+    return ((BuildFile) bzlFile).findSymbolInScope(myElement.getStringContents());
+  }
+
+  @Override
+  public Object[] getVariants() {
+    PsiElement bzlFile = bzlFileReference.resolve();
+    if (!(bzlFile instanceof BuildFile)) {
+      return EMPTY_ARRAY;
+    }
+    CompletionResultsProcessor processor = new CompletionResultsProcessor(myElement, myElement.getQuoteType());
+    ((BuildFile) bzlFile).searchSymbolsInScope(processor, null);
+    return processor.getResults().toArray();
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    ASTNode newNode = PsiUtils.createNewLabel(myElement.getProject(), newElementName);
+    myElement.getNode().replaceChild(myElement.getNode().getFirstChildNode(), newNode);
+    return myElement;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java
new file mode 100644
index 0000000..eb420d5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/LocalReference.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.completion.CompletionResultsProcessor;
+import com.google.idea.blaze.base.lang.buildfile.psi.ReferenceExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.ResolveUtil;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.util.IncorrectOperationException;
+
+import javax.annotation.Nullable;
+
+/**
+ * Reference from a ReferenceExpression to a PsiNamedElement in the same scope.
+ * This includes symbols accessible via 'load' statments.
+ */
+public class LocalReference extends PsiReferenceBase<ReferenceExpression> {
+
+  public LocalReference(ReferenceExpression element) {
+    super(element, new TextRange(0, element.getTextLength()), /*soft*/ false);
+  }
+
+  @Nullable
+  @Override
+  public PsiElement resolve() {
+    String referencedName = myElement.getReferencedName();
+    if (referencedName == null) {
+      return null;
+    }
+    return ResolveUtil.findInScope(myElement, referencedName);
+  }
+
+  @Override
+  public Object[] getVariants() {
+    CompletionResultsProcessor processor = new CompletionResultsProcessor(myElement, QuoteType.NoQuotes);
+    ResolveUtil.searchInScope(myElement, processor);
+    return processor.getResults().toArray();
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    ASTNode oldNode = myElement.getNameElement();
+    if (oldNode != null) {
+      ASTNode newNode = PsiUtils.createNewName(myElement.getProject(), newElementName);
+      myElement.getNode().replaceChild(oldNode, newNode);
+    }
+    return myElement;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java
new file mode 100644
index 0000000..572d9bf
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceFragment.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.PathUtil;
+
+import javax.annotation.Nullable;
+
+/**
+ * The label component preceeding the colon.
+ */
+public class PackageReferenceFragment extends PsiReferenceBase<StringLiteral> {
+
+  public PackageReferenceFragment(LabelReference labelReference) {
+    super(labelReference.getElement(), labelReference.getRangeInElement(), labelReference.isSoft());
+  }
+
+  @Nullable
+  private WorkspacePath getWorkspacePath(String labelString) {
+    if (!labelString.startsWith("//")) {
+      return null;
+    }
+    int colonIndex = labelString.indexOf(':');
+    int endIndex = colonIndex != -1 ? colonIndex : labelString.length();
+    return WorkspacePath.createIfValid(labelString.substring(2, endIndex));
+  }
+
+  @Override
+  public TextRange getRangeInElement() {
+    String rawText = myElement.getText();
+    boolean valid = getWorkspacePath(myElement.getStringContents()) != null;
+    if (!valid) {
+      return TextRange.EMPTY_RANGE;
+    }
+    int endIndex = rawText.indexOf(':');
+    if (endIndex == -1) {
+      endIndex = rawText.length() - 1;
+    }
+    return new TextRange(1, endIndex);
+  }
+
+  @Nullable
+  @Override
+  public BuildFile resolve() {
+    WorkspacePath workspacePath = getWorkspacePath(myElement.getStringContents());
+    return BuildReferenceManager.getInstance(myElement.getProject()).resolveBlazePackage(workspacePath);
+  }
+
+  @Override
+  public Object[] getVariants() {
+    return EMPTY_ARRAY;
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    return myElement; // renaming a BUILD file has no effect on the package label fragments
+  }
+
+  @Override
+  public PsiElement bindToElement(PsiElement element) throws IncorrectOperationException {
+    if (!(element instanceof BuildFile)) {
+      return super.bindToElement(element);
+    }
+    if (element.equals(resolve())) {
+      return myElement;
+    }
+    WorkspacePath newPath = ((BuildFile) element).getPackageWorkspacePath();
+    if (newPath == null) {
+      return myElement;
+    }
+    String labelString = myElement.getStringContents();
+    int colonIndex = labelString.indexOf(':');
+    if (colonIndex != -1) {
+      return handleRename("//" + newPath + labelString.substring(colonIndex));
+    }
+    // need to assume there's an implicit rule name
+    return handleRename("//" + newPath + ":" + PathUtil.getFileName(labelString));
+  }
+
+  private PsiElement handleRename(String newStringContents) {
+    ASTNode node = myElement.getNode();
+    node.replaceChild(node.getFirstChildNode(), PsiUtils.createNewLabel(myElement.getProject(), newStringContents));
+    return myElement;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/QuoteType.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/QuoteType.java
new file mode 100644
index 0000000..40125fd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/QuoteType.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+/**
+ * The type of quotes surrounding a PSI element.
+ */
+public enum QuoteType {
+  Single("'"),
+  Double("\""),
+  TripleSingle("'''"),
+  TripleDouble("\"\"\""),
+  NoQuotes("");
+
+  public final String quoteString;
+
+  QuoteType(String quoteString) {
+    this.quoteString = quoteString;
+  }
+
+  public String wrap(String unquotedText) {
+    return quoteString + unquotedText + quoteString;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java
new file mode 100644
index 0000000..b8144bf
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/references/TargetReference.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.completion.CompletionResultsProcessor;
+import com.google.idea.blaze.base.lang.buildfile.psi.TargetExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.ResolveUtil;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiNamedElement;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.util.IncorrectOperationException;
+
+import javax.annotation.Nullable;
+
+/**
+ * A reference to an earlier declaration of this symbol (to handle cases where a symbol
+ * is the target of multiple assignment statements).
+ */
+public class TargetReference extends PsiReferenceBase<TargetExpression> {
+
+  public TargetReference(TargetExpression element) {
+    super(element, new TextRange(0, element.getTextLength()), /*soft*/ true);
+  }
+
+  @Nullable
+  @Override
+  public PsiElement resolve() {
+    String referencedName = myElement.getName();
+    if (referencedName == null) {
+      return null;
+    }
+    PsiNamedElement target = ResolveUtil.findInScope(myElement, referencedName);
+    return target != null ? target : null;
+  }
+
+  @Override
+  public Object[] getVariants() {
+    CompletionResultsProcessor processor = new CompletionResultsProcessor(myElement, QuoteType.NoQuotes);
+    ResolveUtil.searchInScope(myElement, processor);
+    return processor.getResults().toArray();
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    ASTNode oldNode = myElement.getNameNode();
+    if (oldNode != null) {
+      ASTNode newNode = PsiUtils.createNewName(myElement.getProject(), newElementName);
+      myElement.getNode().replaceChild(oldNode, newNode);
+    }
+    return myElement;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
new file mode 100644
index 0000000..301ce93
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackage.java
@@ -0,0 +1,167 @@
+/*
+ * 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.search;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.history.core.Paths;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PackagePrefixFileSystemItem;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileSystemItem;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.util.PathUtil;
+import com.intellij.util.Processor;
+
+import javax.annotation.Nullable;
+import java.util.Objects;
+
+/**
+ * Defines the files accessible by a given blaze package.
+ */
+public class BlazePackage {
+
+  @Nullable
+  public static BlazePackage getContainingPackage(PsiFileSystemItem file) {
+    if (file instanceof PsiFile) {
+      file = ((PsiFile) file).getOriginalFile();
+    }
+    if (file instanceof BuildFile && file.getName().equals("BUILD")) {
+      return new BlazePackage((BuildFile) file);
+    }
+    return getContainingPackage(getPsiDirectory(file));
+  }
+
+  @Nullable
+  private static PsiDirectory getPsiDirectory(PsiFileSystemItem file) {
+    if (file instanceof PsiDirectory) {
+      return (PsiDirectory) file;
+    }
+    if (file instanceof PsiFile) {
+      return ((PsiFile) file).getContainingDirectory();
+    }
+    if (file instanceof PackagePrefixFileSystemItem) {
+      return ((PackagePrefixFileSystemItem) file).getDirectory();
+    }
+    return null;
+  }
+
+  @Nullable
+  public static BlazePackage getContainingPackage(@Nullable PsiDirectory dir) {
+    while (dir != null) {
+      PsiFile buildFile = dir.findFile("BUILD");
+      if (buildFile != null) {
+        return buildFile instanceof BuildFile ? new BlazePackage((BuildFile) buildFile) : null;
+      }
+      dir = dir.getParentDirectory();
+    }
+    return null;
+  }
+
+  public final BuildFile buildFile;
+
+  private BlazePackage(BuildFile buildFile) {
+    this.buildFile = buildFile;
+  }
+
+  @Nullable
+  public PsiDirectory getContainingDirectory() {
+    return buildFile.getParent();
+  }
+
+  /**
+   * The search scope corresponding to this package (i.e. not crossing package boundaries).
+   * @param onlyBlazeFiles if true, the scope is limited to BUILD and Skylark files.
+   */
+  public GlobalSearchScope getSearchScope(boolean onlyBlazeFiles) {
+    return new BlazePackageSearchScope(this, onlyBlazeFiles);
+  }
+
+  /**
+   * Returns the file path relative to this blaze package, or null if it does lie inside this package
+   */
+  @Nullable
+  public String getPackageRelativePath(String filePath) {
+    String packageFilePath = PathUtil.getParentPath(buildFile.getFilePath());
+    return Paths.relativeIfUnder(filePath, packageFilePath);
+  }
+
+  /**
+   * The path from the blaze package directory to the child file, or null if
+   * the package directory is not an ancestor of the provided file.
+   */
+  @Nullable
+  public String getRelativePathToChild(@Nullable VirtualFile child) {
+    if (child == null) {
+      return null;
+    }
+    String packagePath = PathUtil.getParentPath(buildFile.getFilePath());
+    return Paths.relativeIfUnder(child.getPath(), packagePath);
+  }
+
+  /**
+   * Walks the directory tree, processing all files accessible by this package (i.e. not processing child packages).
+   */
+  public void processPackageFiles(Processor<PsiFile> processor) {
+    PsiDirectory dir = getContainingDirectory();
+    if (dir == null) {
+      return;
+    }
+    processPackageFiles(processor, dir);
+  }
+
+  private static void processPackageFiles(Processor<PsiFile> processor, PsiDirectory directory) {
+    processDirectory(processor, directory);
+    for (PsiDirectory child : directory.getSubdirectories()) {
+      if (!isBlazePackage(child)) {
+        processPackageFiles(processor, directory);
+      }
+    }
+  }
+
+  private static boolean isBlazePackage(PsiDirectory directory) {
+    return directory.findFile("BUILD") != null;
+  }
+
+  private static void processDirectory(Processor<PsiFile> processor, PsiDirectory directory) {
+    for (PsiFile file : directory.getFiles()) {
+      processor.process(file);
+    }
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof BlazePackage)) {
+      return false;
+    }
+    if (obj == this) {
+      return true;
+    }
+    BlazePackage that = (BlazePackage) obj;
+    return Objects.equals(buildFile.getFilePath(), that.buildFile.getFilePath());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(buildFile.getFilePath());
+  }
+
+  @Override
+  public String toString() {
+    return String.format("%s package: %s", Blaze.buildSystemName(buildFile.getProject()), buildFile.getPackageWorkspacePath());
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageSearchScope.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageSearchScope.java
new file mode 100644
index 0000000..750a1be
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageSearchScope.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.search;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.search.GlobalSearchScope;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Objects;
+
+/**
+ * A scope limited to a single blaze/bazel package, which doesn't cross package boundaries.
+ */
+public class BlazePackageSearchScope extends GlobalSearchScope {
+
+  private final BlazePackage blazePackage;
+  private final boolean onlyBlazeFiles;
+
+  public BlazePackageSearchScope(BlazePackage blazePackage, boolean onlyBlazeFiles) {
+    super(blazePackage.buildFile.getProject());
+    this.blazePackage = blazePackage;
+    this.onlyBlazeFiles = onlyBlazeFiles;
+  }
+
+  @Override
+  public boolean contains(@NotNull VirtualFile file) {
+    PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(file);
+    if (onlyBlazeFiles && !(psiFile instanceof BuildFile)) {
+      return false;
+    }
+    return blazePackage.equals(BlazePackage.getContainingPackage(psiFile));
+  }
+
+  @Override
+  public int compare(@NotNull VirtualFile file1, @NotNull VirtualFile file2) {
+    return 0;
+  }
+
+  @Override
+  public boolean isSearchInModuleContent(@NotNull Module aModule) {
+    return true;
+  }
+
+  @Override
+  public boolean isSearchInLibraries() {
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return String.format("%s directory scope: %s", Blaze.buildSystemName(getProject()), blazePackage.buildFile.getPackageWorkspacePath());
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (!(obj instanceof BlazePackageSearchScope)) {
+      return false;
+    }
+    if (obj == this) {
+      return true;
+    }
+    BlazePackageSearchScope other = (BlazePackageSearchScope) obj;
+    return blazePackage.equals(other.blazePackage) && onlyBlazeFiles == other.onlyBlazeFiles;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(blazePackage, onlyBlazeFiles);
+  }
+
+  @Override
+  public String getDisplayName() {
+    return blazePackage.toString();
+  }
+
+  @Override
+  public GlobalSearchScope uniteWith(@NotNull GlobalSearchScope scope) {
+    if (scope instanceof BlazePackageSearchScope) {
+      BlazePackageSearchScope other = (BlazePackageSearchScope) scope;
+      if (!blazePackage.equals(other.blazePackage)) {
+        return GlobalSearchScope.EMPTY_SCOPE;
+      }
+      return onlyBlazeFiles ? this : other;
+    }
+    return super.uniteWith(scope);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.java
new file mode 100644
index 0000000..f8ae2e6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/BuildLabelReferenceSearcher.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.blaze.base.lang.buildfile.search;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile.BlazeFileType;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
+import com.intellij.openapi.application.QueryExecutorBase;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.search.*;
+import com.intellij.psi.search.searches.ReferencesSearch.SearchParameters;
+import com.intellij.util.Processor;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * String search for label references in BUILD files
+ */
+public class BuildLabelReferenceSearcher extends QueryExecutorBase<PsiReference, SearchParameters> {
+
+  public BuildLabelReferenceSearcher() {
+    super(true);
+  }
+
+  @Override
+  public void processQuery(SearchParameters params, Processor<PsiReference> consumer) {
+    PsiElement element = params.getElementToSearch();
+    if (element instanceof FunctionStatement) {
+      String fnName = ((FunctionStatement) element).getName();
+      if (fnName != null) {
+        searchForString(params, element, fnName);
+      }
+      return;
+    }
+    PsiFile file = ResolveUtil.asFileSearch(element);
+    if (file != null) {
+      processFileReferences(params, file);
+      return;
+    }
+
+    if (!(element instanceof FuncallExpression)) {
+      return;
+    }
+
+    Label label = ((FuncallExpression) element).resolveBuildLabel();
+    PsiFile localFile = element.getContainingFile();
+    if (label == null || localFile == null) {
+      return;
+    }
+    List<String> stringsToSearch = LabelUtils.getAllValidLabelStrings(label, true);
+    for (String string : stringsToSearch) {
+      if (string.startsWith("//")) {
+        searchForString(params, element, string);
+      } else {
+        // only a valid reference from local package -- restrict the search scope accordingly
+        SearchScope scope = limitScopeToFile(params.getScopeDeterminedByUser(), localFile);
+        if (scope != null) {
+          searchForString(params, scope, element, string);
+        }
+      }
+    }
+  }
+
+  /**
+   * Find all references to the given file within BUILD files.
+   */
+  private void processFileReferences(SearchParameters params, PsiFile file) {
+    if (file instanceof BuildFile) {
+      BuildFile buildFile = (BuildFile) file;
+      processBuildFileReferences(params, buildFile);
+      if (buildFile.getBlazeFileType() == BlazeFileType.BuildPackage) {
+        return;
+      }
+      // for skylark extensions, we also check for package-local references, below
+    }
+    BlazePackage blazePackage = BlazePackage.getContainingPackage(file);
+    PsiDirectory directory = blazePackage != null ? blazePackage.getContainingDirectory() : null;
+    if (directory == null) {
+      return;
+    }
+    Label label = LabelUtils.createLabelForFile(blazePackage, PsiUtils.getFilePath(file));
+    if (label == null) {
+      return;
+    }
+
+    // files can only be directly referenced in the containing blaze package
+    List<String> stringsToSearch = LabelUtils.getAllValidLabelStrings(label, true);
+    SearchScope scope = params.getScopeDeterminedByUser()
+      .intersectWith(blazePackage.getSearchScope(true));
+
+    for (String string : stringsToSearch) {
+      searchForString(params, scope, file, string);
+    }
+  }
+
+  /**
+   * Find references to both the file itself, and build targets defined in the file.
+   */
+  private void processBuildFileReferences(SearchParameters params, BuildFile file) {
+    WorkspacePath workspacePath = file.getPackageWorkspacePath();
+    if (workspacePath == null) {
+      return;
+    }
+    List<String> stringsToSearch = Lists.newArrayList();
+    if (file.getBlazeFileType() == BlazeFileType.BuildPackage) {
+      stringsToSearch.add("//" + workspacePath);
+    } else {
+      stringsToSearch.add("//" + workspacePath + ":" + file.getName());
+      stringsToSearch.add("//" + workspacePath + "/" + file.getName()); // deprecated load/subinclude format
+    }
+    for (String string : stringsToSearch) {
+      searchForString(params, file, string);
+    }
+  }
+
+  /**
+   * Search for package-local references.<br>
+   * Returns null if the resulting scope is empty
+   */
+  @Nullable
+  private static SearchScope limitScopeToFile(SearchScope scope, PsiFile file) {
+    if (scope instanceof LocalSearchScope) {
+      return ((LocalSearchScope) scope).isInScope(file.getVirtualFile()) ? new LocalSearchScope(file) : null;
+    }
+    return scope.intersectWith(new LocalSearchScope(file));
+  }
+
+  private static void searchForString(SearchParameters params, PsiElement element, String string) {
+    searchForString(params, params.getScopeDeterminedByUser(), element, string);
+  }
+
+  private static void searchForString(SearchParameters params, SearchScope scope, PsiElement element, String string) {
+    if (scope instanceof GlobalSearchScope) {
+      scope = GlobalSearchScope.getScopeRestrictedByFileTypes((GlobalSearchScope) scope, BuildFileType.INSTANCE);
+    }
+    params.getOptimizer().searchWord(string, scope, UsageSearchContext.IN_STRINGS, true, element);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/ExcludeBuildFilesScope.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/ExcludeBuildFilesScope.java
new file mode 100644
index 0000000..60c73a4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/ExcludeBuildFilesScope.java
@@ -0,0 +1,48 @@
+/*
+ * 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.search;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFileSystemItem;
+import com.intellij.psi.search.EverythingGlobalScope;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.UseScopeOptimizer;
+
+import javax.annotation.Nullable;
+
+/**
+ * Causes all calls to PsiSearchHelper.getUseScope to exclude BUILD files, when searching for files.<br>
+ * BUILD file / BUILD package references are handled by a separate reference searcher.<p>
+ *
+ * This is a hack, but greatly improves efficiency. The reasoning behind this:
+ *  - BUILD files have very strict file reference patterns, and very narrow direct reference scopes (a package can't
+ *  directly reference files in another package).
+ *  - IJ *constantly* performs global searches on strings when manipulating files (e.g. searching for file uses for
+ *  highlighting, rename, move operations). This causes us to re-parse every BUILD file in the project, multiple times.
+ *
+ */
+public class ExcludeBuildFilesScope extends UseScopeOptimizer {
+
+  @Nullable
+  @Override
+  public GlobalSearchScope getScopeToExclude(PsiElement element) {
+    if (element instanceof PsiFileSystemItem) {
+      return GlobalSearchScope.getScopeRestrictedByFileTypes(new EverythingGlobalScope(), BuildFileType.INSTANCE);
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/FindUsages.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/FindUsages.java
new file mode 100644
index 0000000..24c9aa8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/FindUsages.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.search;
+
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.PsiSearchHelper;
+import com.intellij.psi.search.SearchScope;
+import com.intellij.psi.search.searches.ReferencesSearch;
+
+/**
+ * Utility methods for finding all references to a PsiElement
+ */
+public class FindUsages {
+
+  public static PsiReference[] findAllReferences(PsiElement element) {
+    return findReferencesInScope(element, GlobalSearchScope.allScope(element.getProject()));
+  }
+
+  /**
+   * Search scope taken from PsiSearchHelper::getUseScope, which incorporates UseScopeEnlarger / UseScopeOptimizer EPs.
+   */
+  public static PsiReference[] findReferencesInElementScope(PsiElement element) {
+    return findReferencesInScope(element, PsiSearchHelper.SERVICE.getInstance(element.getProject()).getUseScope(element));
+  }
+
+  public static PsiReference[] findReferencesInScope(PsiElement element, SearchScope scope) {
+    return ReferencesSearch.search(element, scope, true).toArray(new PsiReference[0]);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/GlobReferenceSearcher.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/GlobReferenceSearcher.java
new file mode 100644
index 0000000..d9e2e58
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/GlobReferenceSearcher.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.search;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.GlobExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.openapi.application.QueryExecutorBase;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFileSystemItem;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.LocalSearchScope;
+import com.intellij.psi.search.SearchScope;
+import com.intellij.psi.search.searches.ReferencesSearch.SearchParameters;
+import com.intellij.util.IncorrectOperationException;
+import com.intellij.util.Processor;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Searches for references to a file in globs. These aren't picked up by a standard string search, and are only
+ * evaluated on demand, so we can't just check a reference cache.<p>
+ *
+ * Unlike resolving a glob, this requires no file system calls (beyond finding the parent blaze package),
+ * because we're only interested in a single file, which is already known to exist.<p>
+ *
+ * This is always a local search (as glob references can't cross package boundaries).
+ */
+public class GlobReferenceSearcher extends QueryExecutorBase<PsiReference, SearchParameters> {
+
+  public GlobReferenceSearcher() {
+    super(true);
+  }
+
+  @Override
+  public void processQuery(SearchParameters queryParameters, Processor<PsiReference> consumer) {
+    PsiFileSystemItem file = ResolveUtil.asFileSystemItemSearch(queryParameters.getElementToSearch());
+    if (file == null) {
+      return;
+    }
+    BlazePackage containingPackage = BlazePackage.getContainingPackage(file);
+    if (containingPackage == null || !inScope(queryParameters, containingPackage.buildFile)) {
+      return;
+    }
+    String relativePath = containingPackage.getRelativePathToChild(file.getVirtualFile());
+    if (relativePath == null) {
+      return;
+    }
+
+    List<GlobExpression> globs = PsiUtils.findAllChildrenOfClassRecursive(containingPackage.buildFile, GlobExpression.class);
+    for (GlobExpression glob : globs) {
+      if (glob.matches(relativePath, file.isDirectory())) {
+        consumer.process(globReference(glob, file));
+      }
+    }
+  }
+
+  private static PsiReference globReference(GlobExpression glob, PsiFileSystemItem file) {
+    return new PsiReferenceBase.Immediate<GlobExpression>(glob, glob.getReferenceTextRange(), file) {
+      @Override
+      public PsiElement bindToElement(@NotNull PsiElement element) throws IncorrectOperationException {
+        return glob;
+      }
+    };
+  }
+
+  private static boolean inScope(SearchParameters queryParameters, BuildFile buildFile) {
+    SearchScope scope = queryParameters.getScopeDeterminedByUser();
+    if (scope instanceof GlobalSearchScope) {
+      return ((GlobalSearchScope) scope).contains(buildFile.getVirtualFile());
+    }
+    return ((LocalSearchScope) scope).isInScope(buildFile.getVirtualFile());
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/PsiFileProvider.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/PsiFileProvider.java
new file mode 100644
index 0000000..0230744
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/PsiFileProvider.java
@@ -0,0 +1,34 @@
+/*
+ * 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.search;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+
+import javax.annotation.Nullable;
+
+/**
+ * Checks if the PsiElement represents a top-level PsiFile (e.g. a top-level java PsiClass can be interchanged with the
+ * corresponding PsiFile, when searching for usages).
+ */
+public interface PsiFileProvider {
+
+  ExtensionPointName<PsiFileProvider> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.PsiFileProvider");
+
+  @Nullable
+  PsiFile asFileSearch(PsiElement elementToSearch);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/ResolveUtil.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/ResolveUtil.java
new file mode 100644
index 0000000..4e3a2e2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/search/ResolveUtil.java
@@ -0,0 +1,166 @@
+/*
+ * 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.search;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileSystemItem;
+import com.intellij.psi.PsiNamedElement;
+import com.intellij.util.Processor;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utilities methods for resolving references
+ */
+public class ResolveUtil {
+
+  /**
+   * Walks up PSI tree of local file, checking PsiNamedElements
+   */
+  public static void searchInScope(PsiElement originalElement, Processor<BuildElement> processor) {
+    // TODO: Handle list comprehension (where variable is defined *later* in the code)
+    boolean topLevelScope = true;
+    PsiElement element = originalElement;
+    while (!(element instanceof PsiFileSystemItem)) {
+      PsiElement parent = element.getParent();
+      if (parent instanceof BuildFile) {
+        if (!((BuildFile) parent).searchSymbolsInScope(processor, topLevelScope ? element : null)) {
+          return;
+        }
+      } else if (parent instanceof FunctionStatement) {
+        topLevelScope = false;
+        for (Parameter param : ((FunctionStatement) parent).getParameters()) {
+          if (!processor.process(param)) {
+            return;
+          }
+        }
+      } else if (parent instanceof ForStatement) {
+        for (Expression expr : ((ForStatement) parent).getForLoopVariables()) {
+          if (expr instanceof TargetExpression && !processor.process(expr)) {
+            return;
+          }
+        }
+      } else if (parent instanceof StatementList)  {
+        if (!visitChildAssignmentStatements((BuildElement) parent, (Processor) processor)) {
+          return;
+        }
+      }
+      element = parent;
+    }
+  }
+
+  /**
+   * Walks up PSI tree of local file, checking PsiNamedElements
+   */
+  @Nullable
+  public static PsiNamedElement findInScope(PsiElement element, String name) {
+    PsiNamedElement[] resultHolder = new PsiNamedElement[1];
+    Processor<BuildElement> processor = buildElement -> {
+      if (buildElement == element) {
+        return true;
+      }
+      if (buildElement instanceof PsiNamedElement && name.equals(buildElement.getName())) {
+        resultHolder[0] = (PsiNamedElement) buildElement;
+        return false;
+      } else if (buildElement instanceof StringLiteral) {
+        StringLiteral stringLiteral = (StringLiteral) buildElement;
+        if (name.equals(stringLiteral.getStringContents())) {
+          PsiElement referencedSymbol = stringLiteral.getReferencedElement();
+          if (referencedSymbol instanceof PsiNamedElement) {
+            resultHolder[0] = (PsiNamedElement) referencedSymbol;
+            return false;
+          }
+        }
+      }
+      return true;
+    };
+    searchInScope(element, processor);
+    return resultHolder[0];
+  }
+
+  /**
+   * @return false if processing was stopped
+   */
+  public static boolean visitChildAssignmentStatements(BuildElement parent, Processor<TargetExpression> processor) {
+    for (AssignmentStatement stmt : parent.childrenOfClass(AssignmentStatement.class)) {
+      TargetExpression target = stmt.getLeftHandSideExpression();
+      if (target != null && !processor.process(target)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Nullable
+  public static TargetExpression searchChildAssignmentStatements(BuildElement parent, String name) {
+    TargetExpression[] resultHolder = new TargetExpression[1];
+    visitChildAssignmentStatements(parent, targetExpr -> {
+      if (name.equals(targetExpr.getName())) {
+        resultHolder[0] = targetExpr;
+        return false;
+      }
+      return true;
+    });
+    return resultHolder[0];
+  }
+
+  /**
+   * @return false if processing was stopped
+   */
+  public static boolean visitLoadedSymbols(BuildFile file, Processor<BuildElement> processor) {
+    for (LoadStatement loadStatement : file.findChildrenByClass(LoadStatement.class)) {
+      for (StringLiteral symbol : loadStatement.getImportedSymbolElements()) {
+        if (!processor.process(symbol)) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  /**
+   * Checks if the element we're searching for is represented by a file or directory.<br>
+   * e.g. a java class PSI element, or an actual PsiFile element.
+   */
+  @Nullable
+  public static PsiFileSystemItem asFileSystemItemSearch(PsiElement elementToSearch) {
+    if (elementToSearch instanceof PsiFileSystemItem) {
+      return (PsiFileSystemItem) elementToSearch;
+    }
+    return asFileSearch(elementToSearch);
+  }
+
+  /**
+   * Checks if the element we're searching for is represented by a file.<br>
+   * e.g. a java class PSI element, or an actual PsiFile element.
+   */
+  @Nullable
+  public static PsiFile asFileSearch(PsiElement elementToSearch) {
+    if (elementToSearch instanceof PsiFile) {
+      return (PsiFile) elementToSearch;
+    }
+    for (PsiFileProvider provider : PsiFileProvider.EP_NAME.getExtensions()) {
+      PsiFile file = provider.asFileSearch(elementToSearch);
+      if (file != null) {
+        return file;
+      }
+    }
+    return null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.java
new file mode 100644
index 0000000..d4405bd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/sync/BuildLangSyncPlugin.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.base.lang.buildfile.sync;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.repackaged.devtools.build.lib.query2.proto.proto2api.Build;
+import com.google.repackaged.protobuf.InvalidProtocolBufferException;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Updates the language specification during the blaze sync process
+ */
+public class BuildLangSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  private static final Logger LOG = Logger.getInstance(BuildLangSyncPlugin.class);
+
+  @Override
+  public void updateSyncState(Project project,
+                              BlazeContext context,
+                              WorkspaceRoot workspaceRoot,
+                              ProjectViewSet projectViewSet,
+                              WorkspaceLanguageSettings workspaceLanguageSettings,
+                              BlazeRoots blazeRoots,
+                              @Nullable WorkingSet workingSet,
+                              WorkspacePathResolver workspacePathResolver,
+                              ImmutableMap<Label, RuleIdeInfo> ruleMap,
+                              @Deprecated @Nullable File androidPlatformDirectory,
+                              SyncState.Builder syncStateBuilder,
+                              @Nullable SyncState previousSyncState) {
+
+    LanguageSpecResult spec = getBuildLanguageSpec(project, workspaceRoot, previousSyncState, context);
+    if (spec != null) {
+      syncStateBuilder.put(LanguageSpecResult.class, spec);
+    }
+  }
+
+  @Nullable
+  private static LanguageSpecResult getBuildLanguageSpec(
+    Project project,
+    WorkspaceRoot workspace,
+    @Nullable SyncState previousSyncState,
+    BlazeContext parentContext) {
+    LanguageSpecResult oldResult = previousSyncState != null ? previousSyncState.get(LanguageSpecResult.class) : null;
+    if (oldResult != null && !oldResult.shouldRecalculateSpec()) {
+      return oldResult;
+    }
+    LanguageSpecResult result = Scope.push(parentContext, (context) -> {
+      context.push(new TimingScope("BUILD language spec"));
+      BuildLanguageSpec spec = parseLanguageSpec(project, workspace, context);
+      if (spec != null) {
+        return new LanguageSpecResult(spec, System.currentTimeMillis());
+      }
+      return null;
+    });
+    return result != null ? result : oldResult;
+  }
+
+  @Nullable
+  private static BuildLanguageSpec parseLanguageSpec(Project project, WorkspaceRoot workspace, BlazeContext context) {
+    try {
+      // it's wasteful converting to a string and back, but uses existing code, and has a very minor cost
+      // (this is only run once per workspace)
+      ListenableFuture<byte[]> future = BlazeInfo.getInstance()
+        .runBlazeInfoGetBytes(context, Blaze.getBuildSystem(project), workspace, ImmutableList.of(), BlazeInfo.BUILD_LANGUAGE);
+
+      return BuildLanguageSpec.fromProto(Build.BuildLanguage.parseFrom(future.get()));
+
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      return null;
+    } catch (ExecutionException | InvalidProtocolBufferException | NullPointerException e) {
+      if (!ApplicationManager.getApplication().isUnitTestMode()) {
+        LOG.error(e);
+      }
+      return null;
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/sync/LanguageSpecResult.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/sync/LanguageSpecResult.java
new file mode 100644
index 0000000..1cd3886
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/sync/LanguageSpecResult.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.sync;
+
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+
+import java.io.Serializable;
+
+/**
+ * The BUILD language specifications, serialized along with the sync data.
+ */
+public class LanguageSpecResult implements Serializable {
+
+  private static final long ONE_DAY_IN_MILLISECONDS = 1000 * 60 * 60;
+
+  public final BuildLanguageSpec spec;
+  public final long timestampMillis;
+
+  public LanguageSpecResult(BuildLanguageSpec spec, long timestampMillis) {
+    this.spec = spec;
+    this.timestampMillis = timestampMillis;
+  }
+
+  public boolean shouldRecalculateSpec() {
+    return System.currentTimeMillis() - timestampMillis > ONE_DAY_IN_MILLISECONDS;
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.java
new file mode 100644
index 0000000..c1ab33a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/BuildAnnotator.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.base.lang.buildfile.validation;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElementVisitor;
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.Annotator;
+import com.intellij.psi.PsiElement;
+
+/**
+ * Base class for Annotator implementations using type-specific methods in BuildElementVisitor
+ */
+public abstract class BuildAnnotator extends BuildElementVisitor implements Annotator {
+
+  private volatile AnnotationHolder holder;
+
+  protected AnnotationHolder getHolder() {
+    return holder;
+  }
+
+  @Override
+  public synchronized void annotate(PsiElement element, AnnotationHolder holder) {
+    this.holder = holder;
+    try {
+      element.accept(this);
+    } finally {
+      this.holder = null;
+    }
+  }
+
+  protected void markError(PsiElement element, String message) {
+    getHolder().createErrorAnnotation(element, message);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/ErrorAnnotator.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/ErrorAnnotator.java
new file mode 100644
index 0000000..2ae4808
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/ErrorAnnotator.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.references.LabelReference;
+import com.intellij.psi.PsiElement;
+
+/**
+ * Additional error annotations, post parsing.<p>
+ * This has been turned off because it's unusable. BuildFile is re-parsed *every* time it's touched, and is never
+ * cached. Until this is fixed, we can't run any annotators touching the file.<p>
+ *
+ * One option: try moving all expensive checks to 'visitFile', so they're not run in parallel
+ */
+public class ErrorAnnotator extends BuildAnnotator {
+
+  @Override
+  public void visitLoadStatement(LoadStatement node) {
+    StringLiteral[] strings = node.getChildStrings();
+    if (strings.length == 0) {
+      return;
+    }
+    PsiElement skylarkRef = new LabelReference(strings[0], false).resolve();
+    if (skylarkRef == null) {
+      markError(strings[0], "Cannot find this Skylark module");
+      return;
+    }
+    if (!(skylarkRef instanceof BuildFile)) {
+      markError(strings[0], strings[0].getText() + " is not a Skylark module");
+      return;
+    }
+    if (strings.length == 1) {
+      markError(node, "No definitions imported from Skylark module");
+      return;
+    }
+    BuildFile skylarkModule = (BuildFile) skylarkRef;
+    for (int i = 1; i < strings.length; i++) {
+      String text = strings[i].getStringContents();
+      FunctionStatement fn = skylarkModule.findDeclaredFunction(text);
+      if (fn == null) {
+        markError(strings[i], "Function '" + text + "' not found in Skylark module " + skylarkModule.getFileName());
+      }
+    }
+  }
+
+  @Override
+  public void visitFuncallExpression(FuncallExpression node) {
+    FunctionStatement function = (FunctionStatement) node.getReferencedElement();
+    if (function == null) {
+      // likely a built-in rule. We don't yet recognize these.
+      return;
+    }
+    // check keyword args match function parameters
+    ParameterList params = function.getParameterList();
+    if (params == null || params.hasStarStar()) {
+      return;
+    }
+    for (Argument arg : node.getArguments()) {
+      if (arg instanceof Argument.Keyword) {
+        String name = arg.getName();
+        if (name != null && params.findParameterByName(name) == null) {
+          markError(arg, "No parameter found in '" + node.getFunctionName() + "' with name '" + name + "'");
+        }
+      }
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobErrorAnnotator.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobErrorAnnotator.java
new file mode 100644
index 0000000..f8fa833
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobErrorAnnotator.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.psi.PsiElement;
+
+import javax.annotation.Nullable;
+
+/**
+ * Checks that glob expressions are valid
+ */
+public class GlobErrorAnnotator extends BuildAnnotator {
+
+  @Override
+  public void visitGlobExpression(GlobExpression node) {
+    Argument[] args = node.getArguments();
+    boolean hasIncludes = false;
+    for (int i = 0; i < args.length; i++) {
+      Argument arg = args[i];
+      String name = arg instanceof Argument.Keyword ? arg.getName() : null;
+      if ("include".equals(name) || (arg instanceof Argument.Positional && i == 0)) {
+        hasIncludes = checkIncludes(arg.getValue());
+      } else if ("exclude".equals(name)) {
+        checkListContents("exclude", arg.getValue());
+      } else if ("exclude_directories".equals(name)) {
+        checkExcludeDirsNode(arg);
+      } else {
+        markError(arg, "Unrecognized glob argument");
+      }
+    }
+    if (!hasIncludes) {
+      markError(node, "Glob expression must contain at least one included string");
+    }
+  }
+
+  private void checkExcludeDirsNode(Argument arg) {
+    Expression value = arg.getValue();
+    if (value == null || !(value.getText().equals("0") || value.getText().equals("1"))) {
+      markError(arg, "exclude_directories parameter to glob must be 0 or 1");
+    }
+  }
+
+  /**
+   * @return true if glob contains at least one included string
+   */
+  private boolean checkIncludes(@Nullable Expression expr) {
+    return checkListContents("include", expr);
+  }
+
+  /**
+   * @return false if 'expr' is known with certainty not to be a list containing at least one string
+   */
+  private boolean checkListContents(String keyword, @Nullable Expression expr) {
+    if (expr == null) {
+      return false;
+    }
+    PsiElement rootElement = PsiUtils.getReferencedTargetValue(expr);
+    if (rootElement instanceof ListLiteral) {
+      return validatePatternList(keyword, ((ListLiteral) rootElement).getChildExpressions());
+    }
+    if (rootElement instanceof ReferenceExpression || !possiblyValidListLiteral(rootElement)) {
+      markError(expr, "Glob parameter '" + keyword + "' must be a list of strings");
+      return false;
+    }
+    // might possibly be a list, default to not showing any errors
+    return true;
+  }
+
+  /**
+   * @return false if 'expr' is known with certainty not to contain at least one string
+   */
+  private boolean validatePatternList(String keyword, Expression[] expressions) {
+    boolean possiblyHasString = false;
+    for (Expression expr : expressions) {
+      PsiElement rootElement = PsiUtils.getReferencedTargetValue(expr);
+      if (rootElement instanceof ReferenceExpression || !possiblyValidStringLiteral(rootElement)) {
+        markError(expr, "Glob parameter '" + keyword + "' must be a list of strings");
+      } else {
+        possiblyHasString = true;
+        if (rootElement instanceof StringLiteral) {
+          validatePattern((StringLiteral) rootElement);
+        }
+      }
+    }
+    return possiblyHasString;
+  }
+
+  private void validatePattern(StringLiteral pattern) {
+    String error = GlobPatternValidator.validate(pattern.getStringContents());
+    if (error != null) {
+      markError(pattern, error);
+    }
+  }
+
+  /**
+   * Returns false iff we know with certainty that the element cannot resolve to a list literal.
+   */
+  private static boolean possiblyValidListLiteral(PsiElement element) {
+    if (element instanceof ListLiteral || element instanceof GlobExpression) {
+      return true; // these evaluate directly to list literals
+    }
+    if (element instanceof LiteralExpression) {
+      return false; // all other literals cannot evaluate to a ListLiteral
+    }
+    if (element instanceof LoadStatement
+        || element instanceof FunctionStatement) {
+      return false;
+    }
+    // everything else treated as possibly evaluating to a list
+    return true;
+  }
+
+  /**
+   * Returns false iff we know with certainty that the element cannot resolve to a string literal.
+   */
+  private static boolean possiblyValidStringLiteral(PsiElement element) {
+    if (element instanceof StringLiteral ) {
+      return true;
+    }
+    if (element instanceof LiteralExpression) {
+      return false; // all other literals cannot evaluate to a StringLiteral
+    }
+    if (element instanceof LoadStatement
+        || element instanceof FunctionStatement
+        || element instanceof GlobExpression) {
+      return false;
+    }
+    // everything else treated as possibly evaluating to a string
+    return true;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobPatternValidator.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobPatternValidator.java
new file mode 100644
index 0000000..46a0752
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/GlobPatternValidator.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import com.google.common.base.Splitter;
+
+import javax.annotation.Nullable;
+
+/**
+ * Support for resolving globs.<p>
+ */
+public class GlobPatternValidator {
+
+  /**
+   * Validate a single glob pattern. If it's invalid, returns an error message.
+   * Otherwise, returns null.<p>
+   */
+  @Nullable
+  public static String validate(String pattern) {
+    String error = checkPatternForError(pattern);
+    if (error != null) {
+      return "Invalid glob pattern: " + error;
+    }
+    return null;
+  }
+
+  @Nullable
+  private static String checkPatternForError(String pattern) {
+    if (pattern.isEmpty()) {
+      return "pattern cannot be empty";
+    }
+    if (pattern.charAt(0) == '/') {
+      return "pattern cannot be absolute";
+    }
+    for (int i = 0; i < pattern.length(); i++) {
+      char c = pattern.charAt(i);
+      switch (c) {
+        case '(': case ')':
+        case '{': case '}':
+        case '[': case ']':
+          return "illegal character '" + c + "'";
+      }
+    }
+    Iterable<String> segments = Splitter.on('/').split(pattern);
+    for (String segment : segments) {
+      if (segment.isEmpty()) {
+        return "empty segment not permitted";
+      }
+      if (segment.equals(".") || segment.equals("..")) {
+        return "segment '" + segment + "' not permitted";
+      }
+      if (segment.contains("**") && !segment.equals("**")) {
+        return "recursive wildcard must be its own segment";
+      }
+    }
+    return null;
+  }
+
+
+
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/HighlightingAnnotator.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/HighlightingAnnotator.java
new file mode 100644
index 0000000..5f5a1bb
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/validation/HighlightingAnnotator.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import com.google.idea.blaze.base.lang.buildfile.highlighting.BuildSyntaxHighlighter;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.Parameter;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.annotation.Annotation;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.util.PsiTreeUtil;
+
+/**
+ * Additional syntax highlighting, based on parsed PSI elements
+ * TODO: Special highlighting for blaze built-in names? (e.g. android_library) -- see PyBuiltInAnnotator
+ */
+public class HighlightingAnnotator extends BuildAnnotator {
+
+  @Override
+  public void visitParameter(Parameter node) {
+    FunctionStatement function = PsiTreeUtil.getParentOfType(node, FunctionStatement.class);
+    if (function != null) {
+      PsiElement anchor = node.hasDefaultValue() ? node.getFirstChild() : node;
+      final Annotation annotation = getHolder().createInfoAnnotation(anchor, null);
+      annotation.setTextAttributes(BuildSyntaxHighlighter.BUILD_PARAMETER);
+    }
+  }
+
+  @Override
+  public void visitKeywordArgument(Argument.Keyword node) {
+    ASTNode keywordNode = node.getNameNode();
+    if (keywordNode != null) {
+      Annotation annotation = getHolder().createInfoAnnotation(keywordNode, null);
+      annotation.setTextAttributes(BuildSyntaxHighlighter.BUILD_KEYWORD_ARG);
+    }
+  }
+
+  @Override
+  public void visitFunctionStatement(FunctionStatement node) {
+    ASTNode nameNode = node.getNameNode();
+    if (nameNode != null) {
+      Annotation annotation = getHolder().createInfoAnnotation(nameNode, null);
+      annotation.setTextAttributes(BuildSyntaxHighlighter.BUILD_FN_DEFINITION);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
new file mode 100644
index 0000000..2042ef9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewElement.java
@@ -0,0 +1,64 @@
+/*
+ * 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.views;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.ListLiteral;
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.structureView.impl.common.PsiTreeElementBase;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+
+/**
+ * Handles nodes in Structure View.
+ */
+public class BuildStructureViewElement extends PsiTreeElementBase<BuildElement> {
+
+  private final BuildElement element;
+
+  public BuildStructureViewElement(BuildElement element) {
+    super(element);
+    this.element = element;
+  }
+
+  @NotNull
+  @Override
+  public Collection<StructureViewTreeElement> getChildrenBase() {
+    if (element instanceof ListLiteral) {
+
+    }
+    if (!(element instanceof BuildFile)) {
+      // TODO: show inner build rules in Skylark .bzl extensions
+      return ImmutableList.of();
+    }
+    ImmutableList.Builder<StructureViewTreeElement> builder = ImmutableList.builder();
+    for (BuildElement child : ((BuildFile) element).findChildrenByClass(BuildElement.class)) {
+      builder.add(new BuildStructureViewElement(child));
+    }
+    return builder.build();
+  }
+
+  @Nullable
+  @Override
+  public String getPresentableText() {
+    return element.getPresentableText();
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewFactory.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewFactory.java
new file mode 100644
index 0000000..3bf0369
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewFactory.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.views;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.ide.structureView.StructureViewBuilder;
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.TreeBasedStructureViewBuilder;
+import com.intellij.lang.PsiStructureViewFactory;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiFile;
+
+import javax.annotation.Nullable;
+
+/**
+ * PsiStructureViewFactory implementation
+ */
+public class BuildStructureViewFactory implements PsiStructureViewFactory {
+  @Override
+  @Nullable
+  public StructureViewBuilder getStructureViewBuilder(final PsiFile psiFile) {
+     if (!(psiFile instanceof BuildFile)) {
+      return null;
+    }
+     return new TreeBasedStructureViewBuilder() {
+      @Override
+      public StructureViewModel createStructureViewModel(@Nullable Editor editor) {
+        return new BuildStructureViewModel((BuildFile) psiFile, editor);
+      }
+    };
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewModel.java b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewModel.java
new file mode 100644
index 0000000..5ce32dd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/buildfile/views/BuildStructureViewModel.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.views;
+
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.intellij.ide.structureView.StructureViewModel;
+import com.intellij.ide.structureView.StructureViewModelBase;
+import com.intellij.ide.structureView.StructureViewTreeElement;
+import com.intellij.ide.util.treeView.smartTree.Sorter;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiFile;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implements structure view for a BUILD file.
+ * TODO: Include inner build rules for Skylark files (when we can identify them -- e.g. via list of blaze rule types)
+ */
+public class BuildStructureViewModel extends StructureViewModelBase
+  implements StructureViewModel.ElementInfoProvider, StructureViewModel.ExpandInfoProvider {
+
+  public BuildStructureViewModel(BuildFile psiFile, @Nullable Editor editor) {
+    this(psiFile, editor, new BuildStructureViewElement(psiFile));
+    withSorters(Sorter.ALPHA_SORTER);
+    withSuitableClasses(FunctionStatement.class, LoadStatement.class, FuncallExpression.class);
+  }
+
+  public BuildStructureViewModel(PsiFile file, @Nullable Editor editor, StructureViewTreeElement element) {
+    super(file, editor, element);
+  }
+
+  @Override
+  public boolean isAlwaysShowsPlus(StructureViewTreeElement element) {
+    final Object value = element.getValue();
+    return value instanceof BuildFile;
+  }
+
+  @Override
+  public boolean isAlwaysLeaf(StructureViewTreeElement element) {
+    return element.getValue() instanceof TargetExpression;
+  }
+
+  @Override
+  public boolean shouldEnterElement(Object element) {
+    return element instanceof BuildFile; // only show top-level elements
+  }
+
+  @Override
+  public boolean isAutoExpand(StructureViewTreeElement element) {
+    return element.getValue() instanceof PsiFile;
+  }
+
+  @Override
+  public boolean isSmartExpand() {
+    return false;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/AdditionalLanguagesCompletionContributor.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/AdditionalLanguagesCompletionContributor.java
new file mode 100644
index 0000000..10184a9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/AdditionalLanguagesCompletionContributor.java
@@ -0,0 +1,63 @@
+/*
+ * 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.projectview.completion;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiListSection;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.projectview.section.sections.AdditionalLanguagesSection;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.patterns.StandardPatterns;
+import com.intellij.util.ProcessingContext;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * Code completion for additional language types.
+ */
+public class AdditionalLanguagesCompletionContributor 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 AdditionalLanguagesCompletionContributor() {
+    extend(
+      CompletionType.BASIC,
+      psiElement()
+        .withLanguage(ProjectViewLanguage.INSTANCE)
+        .inside(
+          psiElement(ProjectViewPsiListSection.class)
+            .withText(StandardPatterns.string().startsWith(AdditionalLanguagesSection.KEY.getName()))),
+      new CompletionProvider<CompletionParameters>() {
+        @Override
+        protected void addCompletions(CompletionParameters parameters, ProcessingContext context, CompletionResultSet result) {
+          for (LanguageClass type : LanguageClass.values()) {
+            result.addElement(LookupElementBuilder.create(type.getName()));
+          }
+        }
+      }
+    );
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java
new file mode 100644
index 0000000..41d9ad9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/ProjectViewKeywordCompletionContributor.java
@@ -0,0 +1,122 @@
+/*
+ * 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.projectview.completion;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
+import com.google.idea.blaze.base.projectview.section.ListSectionParser;
+import com.google.idea.blaze.base.projectview.section.ScalarSectionParser;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.projectview.section.sections.Sections;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.ProcessingContext;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * Completes project view section names.
+ */
+public class ProjectViewKeywordCompletionContributor 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 ProjectViewKeywordCompletionContributor() {
+    extend(
+      CompletionType.BASIC,
+      psiElement()
+        .withLanguage(ProjectViewLanguage.INSTANCE)
+        .withElementType(ProjectViewTokenType.IDENTIFIERS)
+        .andOr(
+          psiElement().afterLeaf("\n"),
+          psiElement().afterLeaf(psiElement().isNull())
+        ),
+      new CompletionProvider<CompletionParameters>() {
+        @Override
+        protected void addCompletions(CompletionParameters parameters, ProcessingContext context, CompletionResultSet result) {
+          result.addAllElements(keywordLookups);
+        }
+      }
+    );
+  }
+
+  private static final List<LookupElement> keywordLookups = getLookups();
+
+  private static List<LookupElement> getLookups() {
+    ImmutableList.Builder<LookupElement> list = ImmutableList.builder();
+    for (SectionParser parser : Sections.getUndeprecatedParsers()) {
+      list.add(forSectionParser(parser));
+    }
+    return list.build();
+  }
+
+  private static LookupElement forSectionParser(SectionParser parser) {
+    return LookupElementBuilder.create(parser.getName())
+      .withInsertHandler(insertDivider(parser));
+  }
+
+  private static InsertHandler<LookupElement> insertDivider(SectionParser parser) {
+    return (context, item) -> {
+      Editor editor = context.getEditor();
+      Document document = editor.getDocument();
+      context.commitDocument();
+
+      String nextTokenText = findNextTokenText(context);
+      if (nextTokenText == null || nextTokenText == "\n") {
+        document.insertString(context.getTailOffset(), getDivider(parser));
+        editor.getCaretModel().moveToOffset(context.getTailOffset());
+      }
+    };
+  }
+
+  private static String getDivider(SectionParser parser) {
+    if (parser instanceof ListSectionParser) {
+      return ":\n  ";
+    }
+    char div = ((ScalarSectionParser) parser).getDivider();
+    return div == ' ' ? String.valueOf(div) : (div + " ");
+  }
+
+  @Nullable
+  protected static String findNextTokenText(final InsertionContext context) {
+    final PsiFile file = context.getFile();
+    PsiElement element = file.findElementAt(context.getTailOffset());
+    while (element != null && element.getTextLength() == 0) {
+      ASTNode next = element.getNode().getTreeNext();
+      element = next != null ? next.getPsi() : null;
+    }
+    return element != null ? element.getText() : null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/WorkspaceTypeCompletionContributor.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/WorkspaceTypeCompletionContributor.java
new file mode 100644
index 0000000..ffb2c85
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/completion/WorkspaceTypeCompletionContributor.java
@@ -0,0 +1,61 @@
+/*
+ * 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.projectview.completion;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiScalarSection;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.section.sections.WorkspaceTypeSection;
+import com.intellij.codeInsight.completion.*;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementBuilder;
+import com.intellij.util.ProcessingContext;
+
+import static com.intellij.patterns.PlatformPatterns.psiElement;
+
+/**
+ * Code completion for workspace types.
+ */
+public class WorkspaceTypeCompletionContributor 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 WorkspaceTypeCompletionContributor() {
+    extend(
+      CompletionType.BASIC,
+      psiElement()
+        .withLanguage(ProjectViewLanguage.INSTANCE)
+        .inside(ProjectViewPsiScalarSection.class)
+        .afterLeaf(psiElement().withText(":").afterLeaf(WorkspaceTypeSection.KEY.getName())),
+      new CompletionProvider<CompletionParameters>() {
+        @Override
+        protected void addCompletions(CompletionParameters parameters, ProcessingContext context, CompletionResultSet result) {
+          for (WorkspaceType type : WorkspaceType.values()) {
+            result.addElement(LookupElementBuilder.create(type.getName()));
+          }
+        }
+      }
+    );
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java
new file mode 100644
index 0000000..fb00ab6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewCommenter.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.formatting;
+
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
+import com.intellij.lang.CodeDocumentationAwareCommenter;
+import com.intellij.psi.PsiComment;
+import com.intellij.psi.tree.IElementType;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Supports (un)commenting lines via IntelliJ
+ */
+public class ProjectViewCommenter implements CodeDocumentationAwareCommenter {
+
+  @Nullable
+  @Override
+  public String getLineCommentPrefix() {
+    return "#";
+  }
+
+  @Nullable
+  @Override
+  public String getBlockCommentPrefix() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getBlockCommentSuffix() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getCommentedBlockCommentPrefix() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getCommentedBlockCommentSuffix() {
+    return null;
+  }
+
+  @Override
+  public IElementType getLineCommentTokenType() {
+    return ProjectViewTokenType.COMMENT;
+  }
+
+  @Override
+  public IElementType getBlockCommentTokenType() {
+    return null;
+  }
+
+  @Override
+  public IElementType getDocumentationCommentTokenType() {
+    return null;
+  }
+
+  @Override
+  public String getDocumentationCommentPrefix() {
+    return null;
+  }
+
+  @Override
+  public String getDocumentationCommentLinePrefix() {
+    return null;
+  }
+
+  @Override
+  public String getDocumentationCommentSuffix() {
+    return null;
+  }
+
+  @Override
+  public boolean isDocumentationComment(PsiComment element) {
+    return false;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewEnterHandler.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewEnterHandler.java
new file mode 100644
index 0000000..6afd0b9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/formatting/ProjectViewEnterHandler.java
@@ -0,0 +1,104 @@
+/*
+ * 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.projectview.formatting;
+
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiFile;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.intellij.codeInsight.editorActions.enter.EnterHandlerDelegateAdapter;
+import com.intellij.ide.DataManager;
+import com.intellij.injected.editor.EditorWindow;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.injection.InjectedLanguageManager;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
+import com.intellij.openapi.editor.actions.SplitLineAction;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiWhiteSpace;
+import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
+
+/**
+ * Inserts indents as appropriate when enter is pressed.
+ */
+public class ProjectViewEnterHandler extends EnterHandlerDelegateAdapter {
+
+  @Override
+  public Result preprocessEnter(PsiFile file,
+                                Editor editor,
+                                Ref<Integer> caretOffset,
+                                Ref<Integer> caretAdvance,
+                                DataContext dataContext,
+                                EditorActionHandler originalHandler) {
+    int offset = caretOffset.get();
+    if (editor instanceof EditorWindow) {
+      file = InjectedLanguageManager.getInstance(file.getProject()).getTopLevelFile(file);
+      editor = InjectedLanguageUtil.getTopLevelEditor(editor);
+      offset = editor.getCaretModel().getOffset();
+    }
+    if (!isApplicable(file, dataContext) || !insertIndent(file, offset)) {
+      return Result.Continue;
+    }
+    int indent = SectionParser.INDENT;
+
+    editor.getCaretModel().moveToOffset(offset);
+    Document doc = editor.getDocument();
+    PsiDocumentManager.getInstance(file.getProject()).commitDocument(doc);
+
+
+    originalHandler.execute(editor, editor.getCaretModel().getCurrentCaret(), dataContext);
+    LogicalPosition position = editor.getCaretModel().getLogicalPosition();
+    if (position.column < indent) {
+      String spaces = StringUtil.repeatSymbol(' ', indent - position.column);
+      doc.insertString(editor.getCaretModel().getOffset(), spaces);
+    }
+    editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(position.line, indent));
+    return Result.Stop;
+  }
+
+  private static boolean isApplicable(PsiFile file, DataContext dataContext) {
+    if (!(file instanceof ProjectViewPsiFile)) {
+      return false;
+    }
+    Boolean isSplitLine = DataManager.getInstance().loadFromDataContext(dataContext, SplitLineAction.SPLIT_LINE_KEY);
+    if (isSplitLine != null) {
+      return false;
+    }
+    return true;
+  }
+
+  private static boolean insertIndent(PsiFile file, int offset) {
+    if (offset == 0) {
+      return false;
+    }
+    PsiElement element = file.findElementAt(offset - 1);
+    while (element != null && element instanceof PsiWhiteSpace) {
+      element = element.getPrevSibling();
+    }
+    if (element == null || element.getText() != ":") {
+      return false;
+    }
+    ASTNode prev = element.getNode().getTreePrev();
+    return prev != null && prev.getElementType() == ProjectViewTokenType.LIST_KEYWORD;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/highlighting/ProjectViewSyntaxHighlighter.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/highlighting/ProjectViewSyntaxHighlighter.java
new file mode 100644
index 0000000..6653e00
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/highlighting/ProjectViewSyntaxHighlighter.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.lang.projectview.highlighting;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewLexer;
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.editor.colors.TextAttributesKey;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterBase;
+import com.intellij.psi.tree.IElementType;
+
+import java.util.Map;
+
+import static com.intellij.openapi.editor.DefaultLanguageHighlighterColors.*;
+
+/**
+ * This class maps tokens to highlighting attributes. Each attribute contains the font properties.
+ */
+public class ProjectViewSyntaxHighlighter extends SyntaxHighlighterBase {
+
+  private static final Map<IElementType, TextAttributesKey> keys = ImmutableMap.of(
+    ProjectViewTokenType.COMMENT, LINE_COMMENT,
+    ProjectViewTokenType.COLON, SEMICOLON,
+    ProjectViewTokenType.IDENTIFIER, IDENTIFIER,
+    ProjectViewTokenType.LIST_KEYWORD, KEYWORD,
+    ProjectViewTokenType.SCALAR_KEYWORD, KEYWORD
+  );
+
+  @Override
+  public Lexer getHighlightingLexer() {
+    return new ProjectViewLexer();
+  }
+
+  @Override
+  public TextAttributesKey[] getTokenHighlights(IElementType iElementType) {
+    return pack(keys.get(iElementType));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/highlighting/ProjectViewSyntaxHighlighterFactory.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/highlighting/ProjectViewSyntaxHighlighterFactory.java
new file mode 100644
index 0000000..b1166f8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/highlighting/ProjectViewSyntaxHighlighterFactory.java
@@ -0,0 +1,32 @@
+/*
+ * 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.projectview.highlighting;
+
+import com.intellij.openapi.fileTypes.SyntaxHighlighter;
+import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+
+/**
+ * Factory for BuildSyntaxHighlighter
+ */
+public class ProjectViewSyntaxHighlighterFactory extends SyntaxHighlighterFactory {
+
+  @Override
+  public SyntaxHighlighter getSyntaxHighlighter(Project project, VirtualFile virtualFile) {
+    return new ProjectViewSyntaxHighlighter();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewFileType.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewFileType.java
new file mode 100644
index 0000000..d372937
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewFileType.java
@@ -0,0 +1,62 @@
+/*
+ * 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.projectview.language;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.fileTypes.LanguageFileType;
+import icons.BlazeIcons;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * Blaze project view file type
+ */
+public class ProjectViewFileType extends LanguageFileType {
+
+  public static final ProjectViewFileType INSTANCE = new ProjectViewFileType();
+
+
+  private ProjectViewFileType() {
+    super(ProjectViewLanguage.INSTANCE);
+  }
+
+  @Override
+  public String getName() {
+    // Warning: this is conflated with Language.myID in several places...
+    // They must be identical.
+    return ProjectViewLanguage.INSTANCE.getID();
+  }
+
+  @Override
+  public String getDescription() {
+    return Blaze.defaultBuildSystemName() + " project view files";
+  }
+
+  @Override
+  public String getDefaultExtension() {
+    // Ideally we'd return a build-system specific extension here, but that would require
+    // a hack to guess the current project, or choosing either the blaze or bazel
+    // extension. Instead don't specify a default extension.
+    return "";
+  }
+
+  @Override
+  @Nullable
+  public Icon getIcon() {
+    return BlazeIcons.Blaze;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewFileTypeFactory.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewFileTypeFactory.java
new file mode 100644
index 0000000..73b2605
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewFileTypeFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.language;
+
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.intellij.openapi.fileTypes.ExtensionFileNameMatcher;
+import com.intellij.openapi.fileTypes.FileNameMatcher;
+import com.intellij.openapi.fileTypes.FileTypeConsumer;
+import com.intellij.openapi.fileTypes.FileTypeFactory;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Factory for ProjectViewFileType
+ */
+public class ProjectViewFileTypeFactory extends FileTypeFactory {
+
+  @Override
+  public void createFileTypes(@NotNull final FileTypeConsumer consumer) {
+    FileNameMatcher[] matchers = ProjectViewStorageManager.VALID_EXTENSIONS.stream()
+      .map(ExtensionFileNameMatcher::new)
+      .toArray(ExtensionFileNameMatcher[]::new);
+    consumer.consume(ProjectViewFileType.INSTANCE, matchers);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewKeywords.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewKeywords.java
new file mode 100644
index 0000000..e22a838
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewKeywords.java
@@ -0,0 +1,64 @@
+/*
+ * 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.projectview.language;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.projectview.section.ListSectionParser;
+import com.google.idea.blaze.base.projectview.section.ScalarSectionParser;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.projectview.section.SectionParser.ItemType;
+import com.google.idea.blaze.base.projectview.section.sections.Sections;
+
+/**
+ * Section parser keywords accepted in project view files.
+ */
+public class ProjectViewKeywords {
+
+  public static final ImmutableMap<String, ListSectionParser> LIST_KEYWORD_MAP = getListKeywordMap();
+  public static final ImmutableMap<String, ScalarSectionParser> SCALAR_KEYWORD_MAP = getScalarKeywordMap();
+  public static final ImmutableMap<String, ItemType> ITEM_TYPES = getItemTypes();
+
+  private static ImmutableMap<String, ListSectionParser> getListKeywordMap() {
+    ImmutableMap.Builder<String, ListSectionParser> builder = ImmutableMap.builder();
+    for (SectionParser parser : Sections.getParsers()) {
+      if (parser instanceof ListSectionParser) {
+        builder.put(parser.getName(), (ListSectionParser) parser);
+      }
+    }
+    return builder.build();
+  }
+
+  /**
+   * We get the parser so we have access to both the keyword and the divider char.
+   */
+  private static ImmutableMap<String, ScalarSectionParser> getScalarKeywordMap() {
+    ImmutableMap.Builder<String, ScalarSectionParser> builder = ImmutableMap.builder();
+    for (SectionParser parser : Sections.getParsers()) {
+      if (parser instanceof ScalarSectionParser) {
+        builder.put(parser.getName(), (ScalarSectionParser) parser);
+      }
+    }
+    return builder.build();
+  }
+
+  private static ImmutableMap<String, ItemType> getItemTypes() {
+    ImmutableMap.Builder<String, ItemType> builder = ImmutableMap.builder();
+    for (SectionParser parser : Sections.getParsers()) {
+      builder.put(parser.getName(), parser.getItemType());
+    }
+    return builder.build();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewLanguage.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewLanguage.java
new file mode 100644
index 0000000..e0ded02
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/language/ProjectViewLanguage.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.language;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.lang.Language;
+
+/**
+ * Blaze project file language
+ */
+public class ProjectViewLanguage extends Language {
+
+  public static final ProjectViewLanguage INSTANCE = new ProjectViewLanguage();
+
+  private ProjectViewLanguage() {
+    super("projectview");
+  }
+
+  @Override
+  public String getDisplayName() {
+    return Blaze.defaultBuildSystemName() + " project view";
+  }
+
+  @Override
+  public boolean isCaseSensitive() {
+    return true;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexer.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexer.java
new file mode 100644
index 0000000..783a692
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexer.java
@@ -0,0 +1,120 @@
+/*
+ * 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.projectview.lexer;
+
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewLexerBase.Token;
+import com.intellij.lexer.LexerBase;
+import com.intellij.psi.tree.IElementType;
+
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Implementation of LexerBase using BuildLexerBase to tokenize the input.
+ */
+public class ProjectViewLexer extends LexerBase {
+
+  private int offsetEnd;
+  private int offsetStart;
+  private CharSequence buffer;
+  private Iterator<Token> tokens;
+  private Token currentToken;
+
+  @Override
+  public void start(CharSequence charSequence, int startOffset, int endOffset, int initialState) {
+    buffer = charSequence;
+    this.offsetEnd = endOffset;
+    this.offsetStart = startOffset;
+
+    ProjectViewLexerBase lexer = new ProjectViewLexerBase(charSequence.subSequence(startOffset, endOffset));
+    checkNoCharactersMissing(charSequence.subSequence(startOffset, endOffset).length(), lexer.getTokens());
+    tokens = lexer.getTokens().iterator();
+    currentToken = null;
+    if (tokens.hasNext()) {
+      currentToken = tokens.next();
+    }
+  }
+
+  /**
+   * Temporary debugging code. We need to tokenize every character in the input string.
+   */
+  private static void checkNoCharactersMissing(int totalLength, List<Token> tokens) {
+    if (!tokens.isEmpty() && tokens.get(tokens.size() - 1).right != totalLength) {
+      String error = String.format("Lengths don't match: %s instead of %s",
+                                   tokens.get(tokens.size() - 1).right,
+                                   totalLength);
+      throw new RuntimeException(error);
+    }
+    int start = 0;
+    for (int i = 0; i < tokens.size(); i++) {
+      Token token = tokens.get(i);
+      if (token.left != start) {
+        throw new RuntimeException("Gap/inconsistency at: " + start);
+      }
+      start = token.right;
+    }
+  }
+
+
+  @Override
+  public int getState() {
+    return 0;
+  }
+
+  @Override
+  public IElementType getTokenType() {
+    if (currentToken != null) {
+      return currentToken.type;
+    }
+    return null;
+  }
+
+  @Override
+  public int getTokenStart() {
+    if (currentToken == null) {
+      return 0;
+    }
+    return currentToken.left + offsetStart;
+  }
+
+  @Override
+  public int getTokenEnd() {
+    if (currentToken == null) {
+      return 0;
+    }
+    return currentToken.right + offsetStart;
+  }
+
+  @Override
+  public void advance() {
+    if (tokens.hasNext()) {
+      currentToken = tokens.next();
+    } else {
+      currentToken = null;
+    }
+  }
+
+  @Override
+  public CharSequence getBufferSequence() {
+    return buffer;
+  }
+
+  @Override
+  public int getBufferEnd() {
+    return offsetEnd;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerBase.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerBase.java
new file mode 100644
index 0000000..3a6378b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerBase.java
@@ -0,0 +1,162 @@
+/*
+ * 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.projectview.lexer;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewKeywords;
+
+import java.util.List;
+
+/**
+ * Lexer for project view files.
+ */
+public class ProjectViewLexerBase {
+
+  @VisibleForTesting
+  static class Token {
+    final ProjectViewTokenType type;
+    final int left;
+    final int right;
+
+    private Token(ProjectViewTokenType type, int left, int right) {
+      this.type = type;
+      this.left = left;
+      this.right = right;
+    }
+  }
+
+  private final List<Token> tokens;
+
+  // Input buffer and position
+  private final char[] buffer;
+  private int pos;
+
+  private int identifierStart = -1;
+  private boolean lineHasPrecedingNonWhitespaceChar = false;
+
+  public ProjectViewLexerBase(CharSequence input) {
+    this.buffer = input.toString().toCharArray();
+    this.tokens = Lists.newArrayList();
+    this.pos = 0;
+    tokenize();
+  }
+
+  public List<Token> getTokens() {
+    return tokens;
+  }
+
+  /**
+   * Performs tokenization of the character buffer of file contents provided to
+   * the constructor.
+   */
+  private void tokenize() {
+    while (pos < buffer.length) {
+      char c = buffer[pos];
+      pos++;
+      switch (c) {
+        case '\n':
+          addPrecedingIdentifier(pos - 1);
+          tokens.add(new Token(ProjectViewTokenType.NEWLINE, pos - 1, pos));
+          lineHasPrecedingNonWhitespaceChar = false;
+          break;
+        case ' ':
+        case '\t':
+        case '\r':
+          addPrecedingIdentifier(pos - 1);
+          handleWhitespace();
+          break;
+        case ':':
+          addPrecedingIdentifier(pos - 1);
+          tokens.add(new Token(ProjectViewTokenType.COLON, pos - 1, pos));
+          break;
+        case '#':
+          if (!lineHasPrecedingNonWhitespaceChar) {
+            addPrecedingIdentifier(pos - 1);
+            addCommentLine(pos - 1);
+            break;
+          }
+          // otherwise '#' treated as part of the identifier; intentional fall-through
+        default:
+          lineHasPrecedingNonWhitespaceChar = true;
+          // all other characters combined into an 'identifier' lexical token
+          if (identifierStart == -1) {
+            identifierStart = pos - 1;
+          }
+      }
+    }
+    addPrecedingIdentifier(pos);
+  }
+
+  private void addPrecedingIdentifier(int end) {
+    if (identifierStart != -1) {
+      tokens.add(new Token(getIdentifierToken(identifierStart, end), identifierStart, end));
+      identifierStart = -1;
+    }
+  }
+
+  private void addCommentLine(int start) {
+    while (pos < buffer.length) {
+      char c = buffer[pos];
+      if (c == '\n') {
+        break;
+      }
+      pos++;
+    }
+    tokens.add(new Token(ProjectViewTokenType.COMMENT, start, pos));
+  }
+
+  /**
+   * If the whitespace is followed by an end-of-line comment or a newline, it's combined with those
+   * tokens.
+   */
+  private void handleWhitespace() {
+    int oldPos = pos - 1;
+    while (pos < buffer.length) {
+      char c = buffer[pos];
+      switch (c) {
+        case ' ': case '\t': case '\r':
+          pos++;
+          break;
+        default:
+          if (lineHasPrecedingNonWhitespaceChar || c == '#' || c == '\n') {
+            tokens.add(new Token(ProjectViewTokenType.WHITESPACE, oldPos, pos));
+          } else {
+            tokens.add(new Token(ProjectViewTokenType.INDENT, oldPos, pos));
+          }
+          return;
+      }
+    }
+    tokens.add(new Token(ProjectViewTokenType.WHITESPACE, oldPos, pos));
+  }
+
+  private ProjectViewTokenType getIdentifierToken(int start, int end) {
+    String string = bufferSlice(start, end);
+    if (ProjectViewKeywords.LIST_KEYWORD_MAP.keySet().contains(string)) {
+      return ProjectViewTokenType.LIST_KEYWORD;
+    }
+    if (ProjectViewKeywords.SCALAR_KEYWORD_MAP.keySet().contains(string)) {
+      return ProjectViewTokenType.SCALAR_KEYWORD;
+    }
+    return ProjectViewTokenType.IDENTIFIER;
+  }
+
+
+  private String bufferSlice(int start, int end) {
+    return new String(this.buffer, start, end - start);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewTokenType.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewTokenType.java
new file mode 100644
index 0000000..80f7495
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewTokenType.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.lang.projectview.lexer;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.TokenSet;
+
+/**
+ * Lexical elements for the project view language.
+ */
+public class ProjectViewTokenType extends IElementType {
+
+  // only start-of-line (ignoring whitespace) comments are valid
+  public static final ProjectViewTokenType COMMENT = create("comment");
+  public static final ProjectViewTokenType WHITESPACE = create("whitespace");
+  public static final ProjectViewTokenType NEWLINE = create("newline");
+  public static final ProjectViewTokenType COLON = create(":");
+
+  // any amount of whitespace at the start of a line, followed by a non-'#', non-newline character
+  public static final ProjectViewTokenType INDENT = create("indent");
+
+  // all remaining characters that aren't preceded by a start-of-line comments
+  public static final ProjectViewTokenType IDENTIFIER = create("identifier");
+
+  public static final ProjectViewTokenType LIST_KEYWORD = create("list_keyword");
+  public static final ProjectViewTokenType SCALAR_KEYWORD = create("scalar_keyword");
+
+  private static ProjectViewTokenType create(String debugName) {
+    return new ProjectViewTokenType(debugName);
+  }
+
+  private ProjectViewTokenType(String debugName) {
+    super(debugName, ProjectViewLanguage.INSTANCE);
+  }
+
+  public static final TokenSet IDENTIFIERS = TokenSet.create(IDENTIFIER, LIST_KEYWORD, SCALAR_KEYWORD);
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/parser/ProjectViewParserDefinition.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/parser/ProjectViewParserDefinition.java
new file mode 100644
index 0000000..902f6e3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/parser/ProjectViewParserDefinition.java
@@ -0,0 +1,96 @@
+/*
+ * 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.projectview.parser;
+
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewLexer;
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewElementType;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewElementTypes;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiFile;
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.ParserDefinition;
+import com.intellij.lang.PsiBuilder;
+import com.intellij.lang.PsiParser;
+import com.intellij.lexer.Lexer;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.FileViewProvider;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.tree.IFileElementType;
+import com.intellij.psi.tree.TokenSet;
+
+/**
+ * Defines the project view file parser
+ */
+public class ProjectViewParserDefinition implements ParserDefinition {
+
+  @Override
+  public Lexer createLexer(Project project) {
+    return new ProjectViewLexer();
+  }
+
+  @Override
+  public PsiParser createParser(Project project) {
+    return (root, builder) -> {
+      PsiBuilder.Marker rootMarker = builder.mark();
+      new ProjectViewPsiParser(builder).parseFile();
+      rootMarker.done(root);
+      return builder.getTreeBuilt();
+    };
+  }
+
+  @Override
+  public IFileElementType getFileNodeType() {
+    return ProjectViewElementTypes.FILE;
+  }
+
+  @Override
+  public TokenSet getWhitespaceTokens() {
+    return TokenSet.create(ProjectViewTokenType.WHITESPACE);
+  }
+
+  @Override
+  public TokenSet getCommentTokens() {
+    return TokenSet.create(ProjectViewTokenType.COMMENT);
+  }
+
+  @Override
+  public TokenSet getStringLiteralElements() {
+    return TokenSet.EMPTY;
+  }
+
+  @Override
+  public PsiElement createElement(ASTNode node) {
+    IElementType type = node.getElementType();
+    if (type instanceof ProjectViewElementType) {
+      return ((ProjectViewElementType) type).createElement(node);
+    }
+    return new ASTWrapperPsiElement(node);
+  }
+
+  @Override
+  public PsiFile createFile(FileViewProvider viewProvider) {
+    return new ProjectViewPsiFile(viewProvider);
+  }
+
+  @Override
+  public SpaceRequirements spaceExistanceTypeBetweenTokens(ASTNode left, ASTNode right) {
+    return null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/parser/ProjectViewPsiParser.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/parser/ProjectViewPsiParser.java
new file mode 100644
index 0000000..2dcc3ea
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/parser/ProjectViewPsiParser.java
@@ -0,0 +1,213 @@
+/*
+ * 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.projectview.parser;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewKeywords;
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewElementType;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewElementTypes;
+import com.google.idea.blaze.base.projectview.section.ScalarSectionParser;
+import com.intellij.lang.PsiBuilder;
+
+import javax.annotation.Nullable;
+
+/**
+ * Project view psi parser.
+ */
+public class ProjectViewPsiParser {
+
+  private final PsiBuilder builder;
+
+  public ProjectViewPsiParser(PsiBuilder builder) {
+    this.builder = builder;
+  }
+
+  public void parseFile() {
+    builder.setDebugMode(true);
+    while (!builder.eof()) {
+      if (matches(ProjectViewTokenType.NEWLINE)) {
+        continue;
+      }
+      parseSection();
+    }
+  }
+
+  /**
+   * A block is one of:
+   * - scalar section
+   * - list section
+   */
+  private void parseSection() {
+    PsiBuilder.Marker marker = builder.mark();
+    if (matches(ProjectViewTokenType.LIST_KEYWORD)) {
+      expect(ProjectViewTokenType.COLON);
+      skipPastNewline();
+      parseListItems();
+      marker.done(ProjectViewElementTypes.LIST_SECTION);
+      return;
+    }
+    if (currentToken() == ProjectViewTokenType.SCALAR_KEYWORD) {
+      ScalarSectionParser parser = ProjectViewKeywords.SCALAR_KEYWORD_MAP.get(builder.getTokenText());
+      if (parser != null) {
+        parseScalarSection(parser);
+        marker.done(ProjectViewElementTypes.SCALAR_SECTION);
+        return;
+      }
+    }
+    // handle each of the error cases
+    if (matches(ProjectViewTokenType.INDENT)) {
+      skipBlockAndError(marker, "Invalid indentation. Indented lines must be preceded by a list keyword");
+      return;
+    }
+    if (matches(ProjectViewTokenType.COLON)) {
+      skipBlockAndError(marker, "Invalid section: lines cannot begin with a colon.");
+      return;
+    }
+    skipBlockAndError(marker, "Unrecognized keyword: " + builder.getTokenText());
+  }
+
+  private void parseListItems() {
+    while (!builder.eof()) {
+      if (matches(ProjectViewTokenType.NEWLINE)) {
+        continue;
+      }
+      if (!matches(ProjectViewTokenType.INDENT)) {
+        return;
+      }
+      PsiBuilder.Marker marker = builder.mark();
+      skipToNewlineToken();
+      marker.done(ProjectViewElementTypes.LIST_ITEM);
+      builder.advanceLexer();
+    }
+  }
+
+  private void parseScalarSection(ScalarSectionParser parser) {
+    boolean whitespaceDivider = builder.rawLookup(1) == ProjectViewTokenType.WHITESPACE;
+    builder.advanceLexer();
+
+    char divider = parser.getDivider();
+    if (divider == ' ') {
+      if (!whitespaceDivider) {
+        builder.error("Whitespace divider expected after '" + parser.getName() + "'");
+        builder.advanceLexer();
+      }
+      parseScalarItem();
+      return;
+    }
+    if (whitespaceDivider || !Character.toString(divider).equals(builder.getTokenText())) {
+      builder.error(String.format("'%s' expected", divider));
+    }
+    if (!whitespaceDivider) {
+      builder.advanceLexer();
+    }
+    parseScalarItem();
+  }
+
+  private void parseScalarItem() {
+    PsiBuilder.Marker marker = builder.mark();
+    skipToNewlineToken();
+    marker.done(ProjectViewElementTypes.SCALAR_ITEM);
+    builder.advanceLexer();
+  }
+
+  /**
+   * Consumes the current token iff it matches the expected type. Otherwise, returns false
+   */
+  private boolean matches(ProjectViewTokenType kind) {
+    if (currentToken() == kind) {
+      builder.advanceLexer();
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Consumes the current token if it's of the expected type. Otherwise, returns false and reports an error.
+   */
+  private boolean expect(ProjectViewTokenType kind) {
+    if (matches(kind)) {
+      return true;
+    }
+    builder.error(String.format("'%s' expected", kind));
+    return false;
+  }
+
+  /**
+   * Checks if the upcoming sequence of tokens match that expected. Doesn't advance the parser.
+   */
+  private boolean atTokenSequence(ProjectViewTokenType... kinds) {
+    for (int i = 0; i < kinds.length; i++) {
+      if (builder.lookAhead(i) != kinds[i]) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Nullable
+  private ProjectViewTokenType currentToken() {
+    return (ProjectViewTokenType) builder.getTokenType();
+  }
+
+  private void skipBlockAndError(PsiBuilder.Marker marker, String message) {
+    skipToNextBlock();
+    marker.error(message);
+  }
+
+  /**
+   * Skip to the start of the next unindented line
+   */
+  private void skipToNextBlock() {
+    while (!builder.eof()) {
+      if (atTokenSequence(ProjectViewTokenType.NEWLINE, ProjectViewTokenType.IDENTIFIER)) {
+        builder.advanceLexer();
+        return;
+      }
+      builder.advanceLexer();
+    }
+  }
+
+  /**
+   * Skip to the start of the next line
+   */
+  private void skipPastNewline() {
+    while (!builder.eof()) {
+      if (matches(ProjectViewTokenType.NEWLINE)) {
+        return;
+      }
+      builder.advanceLexer();
+    }
+  }
+
+  /**
+   * Skip to the end of the current line
+   */
+  private void skipToNewlineToken() {
+    while (!builder.eof()) {
+      if (currentToken() == ProjectViewTokenType.NEWLINE) {
+        return;
+      }
+      builder.advanceLexer();
+    }
+  }
+
+  private void buildTokenElement(ProjectViewElementType type) {
+    PsiBuilder.Marker marker = builder.mark();
+    builder.advanceLexer();
+    marker.done(type);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewElementType.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewElementType.java
new file mode 100644
index 0000000..1c0644f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewElementType.java
@@ -0,0 +1,50 @@
+/*
+ * 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.projectview.psi;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewFileType;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.tree.IElementType;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * IElementTypes used in the AST by the parser (as opposed to the types used by the lexer).<br>
+ * Modelled on IntelliJ's core language conventions.
+ */
+public class ProjectViewElementType extends IElementType {
+
+  private static final Class[] PARAMETER_TYPES = new Class[]{ASTNode.class};
+  private final Class<? extends PsiElement> psiElementClass;
+  private Constructor<? extends PsiElement> constructor;
+
+  public ProjectViewElementType(String name, Class<? extends PsiElement> psiElementClass) {
+    super(name, ProjectViewFileType.INSTANCE.getLanguage());
+    this.psiElementClass = psiElementClass;
+  }
+
+  public PsiElement createElement(ASTNode node) {
+    try {
+      if (constructor == null) {
+        constructor = psiElementClass.getConstructor(PARAMETER_TYPES);
+      }
+      return constructor.newInstance(node);
+    } catch (Exception e) {
+      throw new IllegalStateException("No necessary constructor for " + node.getElementType(), e);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewElementTypes.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewElementTypes.java
new file mode 100644
index 0000000..1dfb3d5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewElementTypes.java
@@ -0,0 +1,34 @@
+/*
+ * 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.projectview.psi;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewFileType;
+import com.intellij.psi.tree.IFileElementType;
+
+/**
+ * Collects the types used by the PsiBuilder to construct the AST
+ */
+public interface ProjectViewElementTypes {
+
+  IFileElementType FILE = new IFileElementType(ProjectViewFileType.INSTANCE.getLanguage());
+
+  ProjectViewElementType LIST_SECTION = new ProjectViewElementType("list_section", ProjectViewPsiListSection.class);
+  ProjectViewElementType SCALAR_SECTION = new ProjectViewElementType("scalar_section", ProjectViewPsiScalarSection.class);
+
+  ProjectViewElementType LIST_ITEM = new ProjectViewElementType("list_item", ProjectViewPsiListItem.class);
+  ProjectViewElementType SCALAR_ITEM = new ProjectViewElementType("scalar_item", ProjectViewPsiScalarItem.class);
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiElement.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiElement.java
new file mode 100644
index 0000000..0bd331b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiElement.java
@@ -0,0 +1,39 @@
+/*
+ * 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.projectview.psi;
+
+import com.intellij.extapi.psi.ASTWrapperPsiElement;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+
+/**
+ * Base psi element for project view files.
+ */
+public abstract class ProjectViewPsiElement extends ASTWrapperPsiElement {
+  public ProjectViewPsiElement(ASTNode node) {
+    super(node);
+  }
+
+  @Override
+  public PsiReference[] getReferences() {
+    return PsiReference.EMPTY_ARRAY;
+  }
+
+  public <P extends PsiElement> P[] childrenOfClass(Class<P> psiClass) {
+    return findChildrenByClass(psiClass);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiFile.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiFile.java
new file mode 100644
index 0000000..48ff619
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiFile.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.psi;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewFileType;
+import com.intellij.extapi.psi.PsiFileBase;
+import com.intellij.openapi.fileTypes.FileType;
+import com.intellij.psi.FileViewProvider;
+
+/**
+ * PSI file for project view file.
+ */
+public class ProjectViewPsiFile extends PsiFileBase {
+
+  public ProjectViewPsiFile(FileViewProvider viewProvider) {
+    super(viewProvider, ProjectViewFileType.INSTANCE.getLanguage());
+  }
+
+  @Override
+  public FileType getFileType() {
+    return ProjectViewFileType.INSTANCE;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListItem.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListItem.java
new file mode 100644
index 0000000..d198325
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListItem.java
@@ -0,0 +1,29 @@
+/*
+ * 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.projectview.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * Psi element for a list item.
+ */
+public class ProjectViewPsiListItem extends ProjectViewPsiSectionItem {
+
+  public ProjectViewPsiListItem(ASTNode node) {
+    super(node);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListSection.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListSection.java
new file mode 100644
index 0000000..b942788
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiListSection.java
@@ -0,0 +1,29 @@
+/*
+ * 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.projectview.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * Psi element for list section.
+ */
+public class ProjectViewPsiListSection extends ProjectViewPsiElement {
+
+  public ProjectViewPsiListSection(ASTNode node) {
+    super(node);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarItem.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarItem.java
new file mode 100644
index 0000000..397dcb9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarItem.java
@@ -0,0 +1,29 @@
+/*
+ * 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.projectview.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * Psi element for a scalar item.
+ */
+public class ProjectViewPsiScalarItem extends ProjectViewPsiSectionItem {
+
+  public ProjectViewPsiScalarItem(ASTNode node) {
+    super(node);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarSection.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarSection.java
new file mode 100644
index 0000000..6d82256
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiScalarSection.java
@@ -0,0 +1,29 @@
+/*
+ * 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.projectview.psi;
+
+import com.intellij.lang.ASTNode;
+
+/**
+ * Psi element for scalar section.
+ */
+public class ProjectViewPsiScalarSection extends ProjectViewPsiElement {
+
+  public ProjectViewPsiScalarSection(ASTNode node) {
+    super(node);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiSectionItem.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiSectionItem.java
new file mode 100644
index 0000000..d827f10
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/ProjectViewPsiSectionItem.java
@@ -0,0 +1,72 @@
+/*
+ * 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.projectview.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.references.FileLookupData.PathFormat;
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewKeywords;
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewTokenType;
+import com.google.idea.blaze.base.lang.projectview.references.ProjectViewLabelReference;
+import com.google.idea.blaze.base.projectview.section.SectionParser.ItemType;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.SharedPsiElementImplUtil;
+
+import javax.annotation.Nullable;
+
+/**
+ * Psi element for a list or scalar item.
+ */
+public abstract class ProjectViewPsiSectionItem extends ProjectViewPsiElement {
+
+  public ProjectViewPsiSectionItem(ASTNode node) {
+    super(node);
+  }
+
+  @Override
+  public PsiReference[] getReferences() {
+    return SharedPsiElementImplUtil.getReferences(this);
+  }
+
+  @Override
+  public PsiReference getReference() {
+    ASTNode identifier = getNode().findChildByType(ProjectViewTokenType.IDENTIFIER);
+    PathFormat pathFormat = getLabelType();
+    if (identifier != null && pathFormat != null) {
+      return new ProjectViewLabelReference(this, pathFormat);
+    }
+    return null;
+  }
+
+  @Nullable
+  public PathFormat getLabelType() {
+    ASTNode parent = getNode().getTreeParent();
+    ASTNode identifier = parent != null ? parent.getFirstChildNode() : null;
+    if (identifier == null) {
+      return null;
+    }
+    ItemType itemType = ProjectViewKeywords.ITEM_TYPES.get(identifier.getText());
+    if (itemType == null) {
+      return null;
+    }
+    switch (itemType) {
+      case Label: return PathFormat.NonLocal;
+      case FileSystemItem: return PathFormat.NonLocalWithoutInitialBackslashes;
+      default: return null;
+    }
+  }
+
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/util/ProjectViewElementGenerator.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/util/ProjectViewElementGenerator.java
new file mode 100644
index 0000000..ccf5996
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/psi/util/ProjectViewElementGenerator.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.psi.util;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewFileType;
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiElement;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiSectionItem;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiFileFactory;
+import com.intellij.psi.impl.PsiFileFactoryImpl;
+import com.intellij.testFramework.LightVirtualFile;
+
+import javax.annotation.Nullable;
+
+/**
+ * Creates dummy BuildElements, e.g. for renaming purposes.
+ */
+public class ProjectViewElementGenerator {
+
+  private static final String DUMMY_FILENAME = "dummy.bazelproject";
+
+  private static PsiFile createDummyFile(Project project, String contents) {
+    PsiFileFactory factory = PsiFileFactory.getInstance(project);
+    LightVirtualFile virtualFile = new LightVirtualFile(DUMMY_FILENAME, ProjectViewFileType.INSTANCE, contents);
+    PsiFile psiFile = ((PsiFileFactoryImpl) factory).trySetupPsiForFile(virtualFile, ProjectViewLanguage.INSTANCE, false, true);
+    assert psiFile != null;
+    return psiFile;
+  }
+
+  @Nullable
+  public static ASTNode createReplacementItemNode(ProjectViewPsiSectionItem sectionItem, String newStringContents) {
+    TextRange itemRange = sectionItem.getTextRange();
+    ProjectViewPsiElement parent = (ProjectViewPsiElement) sectionItem.getParent();
+    if (parent == null) {
+      return sectionItem.getNode();
+    }
+    int startOffset = sectionItem.getStartOffsetInParent();
+    String originalSectionText = parent.getText();
+    String newSectionText = StringUtil.replaceSubstring(originalSectionText,
+                                                        new TextRange(startOffset, startOffset + itemRange.getLength()),
+                                                        newStringContents);
+    PsiFile dummyFile = createDummyFile(sectionItem.getProject(), newSectionText);
+    PsiElement leafElement = dummyFile.findElementAt(startOffset);
+    return leafElement != null ? leafElement.getParent().getNode() : null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/lang/projectview/references/ProjectViewLabelReference.java b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/references/ProjectViewLabelReference.java
new file mode 100644
index 0000000..431f481
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/lang/projectview/references/ProjectViewLabelReference.java
@@ -0,0 +1,136 @@
+/*
+ * 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.projectview.references;
+
+import com.google.idea.blaze.base.lang.buildfile.completion.BuildLookupElement;
+import com.google.idea.blaze.base.lang.buildfile.completion.LabelRuleLookupElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.references.BuildReferenceManager;
+import com.google.idea.blaze.base.lang.buildfile.references.FileLookupData;
+import com.google.idea.blaze.base.lang.buildfile.references.FileLookupData.PathFormat;
+import com.google.idea.blaze.base.lang.buildfile.references.LabelUtils;
+import com.google.idea.blaze.base.lang.buildfile.references.QuoteType;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiSectionItem;
+import com.google.idea.blaze.base.lang.projectview.psi.util.ProjectViewElementGenerator;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.intellij.lang.ASTNode;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReferenceBase;
+import com.intellij.util.ArrayUtil;
+import com.intellij.util.IncorrectOperationException;
+
+import javax.annotation.Nullable;
+
+/**
+ * A blaze label reference.
+ */
+public class ProjectViewLabelReference extends PsiReferenceBase<ProjectViewPsiSectionItem> {
+
+  private final PathFormat pathFormat;
+
+  public ProjectViewLabelReference(ProjectViewPsiSectionItem element, PathFormat pathFormat) {
+    super(element, new TextRange(0, element.getTextLength()));
+    this.pathFormat = pathFormat;
+  }
+
+  @Nullable
+  @Override
+  public PsiElement resolve() {
+    Label label = getLabel(myElement.getText());
+    if (label == null) {
+      return null;
+    }
+    return BuildReferenceManager.getInstance(myElement.getProject()).resolveLabel(label);
+  }
+
+  @Nullable
+  private static Label getLabel(@Nullable String labelString) {
+    if (labelString == null || !labelString.startsWith("//") || labelString.indexOf('*') != -1) {
+      return null;
+    }
+    return LabelUtils.createLabelFromString(null, labelString);
+  }
+
+  @Override
+  public Object[] getVariants() {
+    String labelString = LabelUtils.trimToDummyIdentifier(myElement.getText());
+    return ArrayUtil.mergeArrays(
+      getRuleLookups(labelString),
+      getFileLookups(labelString));
+  }
+
+  private BuildLookupElement[] getRuleLookups(String labelString) {
+    if (!labelString.startsWith("//") || !labelString.contains(":")) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    String packagePrefix = LabelUtils.getPackagePathComponent(labelString);
+    BuildFile referencedBuildFile =  BuildReferenceManager.getInstance(myElement.getProject()).resolveBlazePackage(packagePrefix);
+    if (referencedBuildFile == null) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    return LabelRuleLookupElement.collectAllRules(referencedBuildFile, labelString, packagePrefix, null, QuoteType.NoQuotes);
+  }
+
+  private BuildLookupElement[] getFileLookups(String labelString) {
+    if (pathFormat == PathFormat.NonLocalWithoutInitialBackslashes) {
+      labelString = StringUtil.trimStart(labelString, "-");
+    }
+    FileLookupData lookupData = FileLookupData.nonLocalFileLookup(labelString, null, QuoteType.NoQuotes, pathFormat);
+    if (lookupData == null) {
+      return BuildLookupElement.EMPTY_ARRAY;
+    }
+    return BuildReferenceManager.getInstance(myElement.getProject()).resolvePackageLookupElements(lookupData);
+  }
+
+  @Override
+  public PsiElement bindToElement(PsiElement element) throws IncorrectOperationException {
+    return myElement;
+  }
+
+  @Override
+  public PsiElement handleElementRename(String newElementName) throws IncorrectOperationException {
+    String currentString = myElement.getText();
+    Label label = getLabel(currentString);
+    if (label == null) {
+      return myElement;
+    }
+    String ruleName = label.ruleName().toString();
+    String newRuleName = newElementName;
+
+    // handle subdirectories
+    int lastSlashIndex = ruleName.lastIndexOf('/');
+    if (lastSlashIndex != -1) {
+      newRuleName = ruleName.substring(0, lastSlashIndex + 1) + newElementName;
+    }
+
+    String packageString = LabelUtils.getPackagePathComponent(currentString);
+    if (packageString.isEmpty() && !currentString.contains(":")) {
+      return handleRename(newRuleName);
+    }
+    return handleRename(packageString + ":" + newRuleName);
+  }
+
+  private PsiElement handleRename(String newStringContents) {
+    ASTNode replacement = ProjectViewElementGenerator.createReplacementItemNode(myElement, newStringContents);
+    if (replacement != null) {
+      myElement.getNode().replaceAllChildrenToChildrenOf(replacement);
+    }
+    return myElement;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/metrics/Action.java b/blaze-base/src/com/google/idea/blaze/base/metrics/Action.java
new file mode 100644
index 0000000..7faa893
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/metrics/Action.java
@@ -0,0 +1,85 @@
+/*
+ * 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.metrics;
+
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An item that can be logged. All actions contain a name that is used as a primary key. The name
+ * should be immutable forever to keep the logs sane.
+ * <p/>
+ * The name used by each {@link Action} should be [a-zA-Z0-9]* to keep things robust since various
+ * log back ends may have different rules about what may or may not be in a key.
+ * <p/>
+ * Do not use any of the following retired values for enums:
+ * INDEX_TOTAL_TIME("index")
+ * REBUILD_TOTAL_TIME("rtt")
+ * SYNC_SAVE_FILES("ssf")
+ * SYNC_COMPUTE_MODULE_DIFF("scmd")
+ * RUN_TOTAL_TIME("ttrp")
+ * DEBUG_TOTAL_TIME("ttsbp")
+ * RUN_TOTAL_TIME_FOR_ANDROID_TEST("ttrpat")
+ * DEBUG_TOTAL_TIME_FOR_ANDROID_TEST("ttsbpat")
+ * IMPORT_TOTAL_TIME("tip")
+ * IDE_BUILD_INFO_RESPONSE("ibi")
+ * RULES_EXTRACTION("re")
+ * BLAZE_MODULES_CREATION("mvc")
+ * INTELLIJ_MODULE_CREATION("imc")
+ * SYNC_RESET_PROJECT("srp")
+ * <p/>
+ */
+public enum Action {
+
+  MAKE_PROJECT_TOTAL_TIME("mtt"),
+  MAKE_MODULE_TOTAL_TIME("mmtt"),
+
+  SYNC_TOTAL_TIME("stt"),
+  SYNC_IMPORT_DATA_TIME("sidt"),
+  BLAZE_BUILD_DURING_SYNC("bb"),
+  BLAZE_BUILD("bld"),
+
+  APK_BUILD_AND_INSTALL("apkbi"),
+
+  BLAZE_COMMAND_USAGE("ttrpbc"),
+
+  OPEN_IN_CODESEARCH("oics"),
+  COPY_GOOGLE3_PATH("cg3p"),
+  OPEN_CORRESPONDING_BUILD_FILE("ocbf"),
+
+  CREATE_BLAZE_RULE("cbr"),
+  CREATE_BLAZE_PACKAGE("cbp"),
+
+  SYNC_SDK("ssdk"),
+
+  C_RESOLVE_FILE("crf"),
+  BLAZE_CLION_TEST_RUN("ctr"),
+  BLAZE_CLION_TEST_DEBUG("ctd")
+  ;
+
+  @NotNull
+  @NonNls
+  final private String name;
+
+  Action(@NotNull String name) {
+    this.name = name;
+  }
+
+  @NotNull
+  public String getName() {
+    return name;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/metrics/LoggingService.java b/blaze-base/src/com/google/idea/blaze/base/metrics/LoggingService.java
new file mode 100644
index 0000000..01ecc7a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/metrics/LoggingService.java
@@ -0,0 +1,63 @@
+/*
+ * 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.metrics;
+
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+/**
+ * Logging service that handles logging timing, hit, and other events to an external sink for
+ * later analysis.
+ */
+public interface LoggingService {
+
+  ExtensionPointName<LoggingService> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.LoggingService");
+
+  /**
+   * Report a value for an event to the available logging services.
+   *  @param variable The variable to report to. Once a value is selected for a logical
+   *                 measurement, the variable's name should never change, even if the colloquial name for the
+   *                 variable changes.
+   */
+  static void reportEvent(Project project, Action variable) {
+    reportEvent(project, variable, 0);
+  }
+
+  /**
+   * Report a value for an event to the available logging services.
+   *  @param variable The variable to report to. Once a value is selected for a logical
+   *                 measurement, the variable's name should never change, even if the colloquial name for the
+   *                 variable changes.
+   * @param value    should be >= 0, set the value to 0 if the value is meaningless
+   */
+  static void reportEvent(Project project, Action variable, long value) {
+    for (LoggingService service : EP_NAME.getExtensions()) {
+      service.doReportEvent(project, variable, value);
+    }
+  }
+
+  /**
+   * Report a value for an event to the logging service
+   *  @param variable The variable to report to. Once a value is selected for a logical
+   *                 measurement, the variable's name should never change, even if the colloquial name for the
+   *                 variable changes.
+   * @param value    should be >= 0, set the value to 0 if the value is meaningless
+   */
+  void doReportEvent(@Nullable Project project, Action variable, long value);
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/BlazeProjectData.java b/blaze-base/src/com/google/idea/blaze/base/model/BlazeProjectData.java
new file mode 100644
index 0000000..4b4e1a9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/BlazeProjectData.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.model;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import java.io.Serializable;
+
+/**
+ * The top-level object serialized to cache.
+ */
+@Immutable
+public class BlazeProjectData implements Serializable {
+  private static final long serialVersionUID = 18L;
+
+  public final long syncTime;
+  public final ImmutableMap<Label, RuleIdeInfo> ruleMap;
+  public final BlazeRoots blazeRoots;
+  @Nullable
+  public final WorkingSet workingSet;
+  public final WorkspacePathResolver workspacePathResolver;
+  public final WorkspaceLanguageSettings workspaceLanguageSettings;
+  public final SyncState syncState;
+  public final ImmutableMultimap<Label, Label> reverseDependencies;
+
+  public BlazeProjectData(
+    long syncTime,
+    ImmutableMap<Label, RuleIdeInfo> ruleMap,
+    BlazeRoots blazeRoots,
+    @Nullable WorkingSet workingSet,
+    WorkspacePathResolver workspacePathResolver,
+    WorkspaceLanguageSettings workspaceLangaugeSettings,
+    SyncState syncState,
+    ImmutableMultimap<Label, Label> reverseDependencies
+  ) {
+    this.syncTime = syncTime;
+    this.ruleMap = ruleMap;
+    this.blazeRoots = blazeRoots;
+    this.workingSet = workingSet;
+    this.workspacePathResolver = workspacePathResolver;
+    this.workspaceLanguageSettings = workspaceLangaugeSettings;
+    this.syncState = syncState;
+    this.reverseDependencies = reverseDependencies;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/BlazeWorkspaceType.java b/blaze-base/src/com/google/idea/blaze/base/model/BlazeWorkspaceType.java
new file mode 100644
index 0000000..46380bf
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/BlazeWorkspaceType.java
@@ -0,0 +1,22 @@
+/*
+ * 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.model;
+
+public enum BlazeWorkspaceType {
+  BASE,
+  JAVA,
+  ANDROID
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/SyncState.java b/blaze-base/src/com/google/idea/blaze/base/model/SyncState.java
new file mode 100644
index 0000000..d91ac1f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/SyncState.java
@@ -0,0 +1,55 @@
+/*
+ * 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.model;
+
+import com.google.common.collect.ImmutableMap;
+
+import javax.annotation.Nullable;
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * Used to save arbitrary state with the sync task.
+ */
+public class SyncState implements Serializable {
+  private static final long serialVersionUID = 1L;
+  private final ImmutableMap<String, Serializable> syncStateMap;
+
+  @SuppressWarnings("unchecked")
+  @Nullable
+  public <T extends Serializable> T get(Class<T> klass) {
+    return (T) syncStateMap.get(klass.getName());
+  }
+
+  public static class Builder {
+    ImmutableMap.Builder<Class, Serializable> syncStateMap = ImmutableMap.builder();
+    public <K extends Serializable, V extends K> Builder put(Class<K> klass, V instance) {
+      syncStateMap.put(klass, instance);
+      return this;
+    }
+    public SyncState build() {
+      return new SyncState(syncStateMap.build());
+    }
+  }
+
+  SyncState(ImmutableMap<Class, Serializable> syncStateMap) {
+    ImmutableMap.Builder<String, Serializable> extraProjectSyncStateMap = ImmutableMap.builder();
+    for (Map.Entry<Class, Serializable> entry : syncStateMap.entrySet()) {
+      extraProjectSyncStateMap.put(entry.getKey().getName(), entry.getValue());
+    }
+    this.syncStateMap = extraProjectSyncStateMap.build();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
new file mode 100644
index 0000000..1ae5775
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/ExecutionRootPath.java
@@ -0,0 +1,131 @@
+/*
+ * 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.model.primitives;
+
+import com.google.common.base.Objects;
+import com.intellij.openapi.util.io.FileUtil;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.Serializable;
+
+/**
+ * An absolute or relative path returned from Blaze. If it is a relative path, it is relative to the execution root.
+ */
+public final class ExecutionRootPath implements Serializable {
+  public static final long serialVersionUID = 3L;
+
+  private final File path;
+
+  public ExecutionRootPath(String path) {
+    this.path = new File(path);
+  }
+
+  public ExecutionRootPath(File path) {
+    this.path = path;
+  }
+
+  public File getAbsoluteOrRelativeFile() {
+    return path;
+  }
+
+  public File getFileRootedAt(File absoluteRoot) {
+    if (path.isAbsolute()) {
+      return path;
+    }
+    return new File(absoluteRoot, path.getPath());
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    ExecutionRootPath that = (ExecutionRootPath)o;
+    return Objects.equal(path, that.path);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(path);
+  }
+
+  @Override
+  public String toString() {
+    return "ExecutionRootPath{" +
+           "path='" + path + '\'' +
+           '}';
+  }
+
+  /**
+   * Returns the relative {@link ExecutionRootPath} if {@code root} is an ancestor of {@code path} otherwise returns null.
+   */
+  @Nullable
+  public static ExecutionRootPath createAncestorRelativePath(File root, File path) {
+    // We cannot find the relative path between an absolute and relative path. The underlying code will make the relative path absolute
+    // by rooting it at the current working directory which is almost never what you want.
+    if (root.isAbsolute() != path.isAbsolute()) {
+      return null;
+    }
+    if (!isAncestor(root.getPath(), path.getPath(), false /* strict */)) {
+      return null;
+    }
+    String relativePath = FileUtil.getRelativePath(root, path);
+    if (relativePath == null) {
+      return null;
+    }
+    return new ExecutionRootPath(new File(relativePath));
+  }
+
+  /**
+   * @param possibleParent
+   * @param possibleChild
+   * @param strict if {@code false} then this method returns {@code true} if {@code possibleParent} equals to {@code possibleChild}.
+   */
+  public static boolean isAncestor(ExecutionRootPath possibleParent, ExecutionRootPath possibleChild, boolean strict) {
+    return isAncestor(possibleParent.getAbsoluteOrRelativeFile().getPath(), possibleChild.getAbsoluteOrRelativeFile().getPath(), strict);
+  }
+
+  /**
+   * @param possibleParentPath
+   * @param possibleChild
+   * @param strict if {@code false} then this method returns {@code true} if {@code possibleParent} equals to {@code possibleChild}.
+   */
+  public static boolean isAncestor(String possibleParentPath, ExecutionRootPath possibleChild, boolean strict) {
+    return isAncestor(possibleParentPath, possibleChild.getAbsoluteOrRelativeFile().getPath(), strict);
+  }
+
+  /**
+   * @param possibleParent
+   * @param possibleChildPath
+   * @param strict if {@code false} then this method returns {@code true} if {@code possibleParent} equals to {@code possibleChild}.
+   */
+  public static boolean isAncestor(ExecutionRootPath possibleParent, String possibleChildPath, boolean strict) {
+    return isAncestor(possibleParent.getAbsoluteOrRelativeFile().getPath(), possibleChildPath, strict);
+  }
+
+  /**
+   * @param possibleParentPath
+   * @param possibleChildPath
+   * @param strict if {@code false} then this method returns {@code true} if {@code possibleParent} equals to {@code possibleChild}.
+   */
+  public static boolean isAncestor(String possibleParentPath, String possibleChildPath, boolean strict) {
+    return FileUtil.isAncestor(possibleParentPath, possibleChildPath, strict);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/Kind.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/Kind.java
new file mode 100644
index 0000000..2272a1f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/Kind.java
@@ -0,0 +1,95 @@
+/*
+ * 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.model.primitives;
+
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Wrapper around a string for a blaze kind (android_library, android_test...)
+ */
+public enum Kind {
+
+  ANDROID_BINARY("android_binary", LanguageClass.ANDROID),
+  ANDROID_LIBRARY("android_library", LanguageClass.ANDROID),
+  ANDROID_TEST("android_test", LanguageClass.ANDROID),
+  ANDROID_ROBOLECTRIC_TEST("android_robolectric_test", LanguageClass.ANDROID),
+  JAVA_LIBRARY("java_library", LanguageClass.JAVA),
+  JAVA_TEST("java_test", LanguageClass.JAVA),
+  JAVA_BINARY("java_binary", LanguageClass.JAVA),
+  JAVA_IMPORT("java_import", LanguageClass.JAVA),
+  JAVA_TOOLCHAIN("java_toolchain", LanguageClass.JAVA),
+  PROTO_LIBRARY("proto_library", LanguageClass.JAVA), // The LanguageClass might have to change if we support other languages
+  JAVA_PLUGIN("java_plugin", LanguageClass.JAVA),
+  ANDROID_RESOURCES("android_resources", LanguageClass.ANDROID),
+  CC_LIBRARY("cc_library", LanguageClass.C),
+  CC_BINARY("cc_binary", LanguageClass.C),
+  CC_TEST("cc_test", LanguageClass.C),
+  CC_INC_LIBRARY("cc_inc_library", LanguageClass.C),
+  CC_TOOLCHAIN("cc_toolchain", LanguageClass.C),
+  JAVA_WRAP_CC("java_wrap_cc", LanguageClass.JAVA),
+  GWT_APPLICATION("gwt_application", LanguageClass.JAVA),
+  GWT_HOST("gwt_host", LanguageClass.JAVA),
+  GWT_MODULE("gwt_module", LanguageClass.JAVA),
+  GWT_TEST("gwt_test", LanguageClass.JAVA),
+  ;
+
+  static final ImmutableMap<String, Kind> STRING_TO_KIND = makeStringToKindMap();
+
+  private static ImmutableMap<String, Kind> makeStringToKindMap() {
+    ImmutableMap.Builder<String, Kind> result = ImmutableMap.builder();
+    for (Kind kind : Kind.values()) {
+      result.put(kind.toString(), kind);
+    }
+    return result.build();
+  }
+
+  public static Kind fromString(String kindString) {
+    return STRING_TO_KIND.get(kindString);
+  }
+
+  private final String kind;
+  private final LanguageClass languageClass;
+
+  Kind(String kind, LanguageClass languageClass) {
+    this.kind = kind;
+    this.languageClass = languageClass;
+  }
+
+  @Override
+  public String toString() {
+    return kind;
+  }
+
+  public LanguageClass getLanguageClass() {
+    return languageClass;
+  }
+
+  public boolean isOneOf(Kind... kinds) {
+    return isOneOf(Arrays.asList(kinds));
+  }
+
+  public boolean isOneOf(List<Kind> kinds) {
+    for (Kind kind : kinds) {
+      if (this.equals(kind)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/Label.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/Label.java
new file mode 100644
index 0000000..c4bf28a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/Label.java
@@ -0,0 +1,153 @@
+/*
+ * 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.model.primitives;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import com.intellij.openapi.diagnostic.Logger;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Wrapper around a string for a blaze label (//package:rule).
+ */
+@Immutable
+public final class Label extends TargetExpression {
+  private static final Logger LOG = Logger.getInstance(Label.class);
+
+  public static final Comparator<Label> COMPARATOR = (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.toString(), o2.toString());
+
+  public static final long serialVersionUID = 2L;
+
+  /**
+   * Silently returns null if this is not a valid Label
+   */
+  @Nullable
+  public static Label createIfValid(String label) {
+    if (validate(label)) {
+      return new Label(label);
+    }
+    return null;
+  }
+
+  public Label(String label) {
+    super(label);
+    List<BlazeValidationError> errors = Lists.newArrayList();
+    if (!validate(label, errors)) {
+      BlazeValidationError.throwError(errors);
+    }
+  }
+
+  public Label(
+    WorkspacePath packageName,
+    RuleName newRuleName) {
+    this("//" + packageName.toString() + ":" + newRuleName.toString());
+  }
+
+  public static boolean validate(String label) {
+    return validate(label, null);
+  }
+
+  public static boolean validate(String label, @Nullable Collection<BlazeValidationError> errors) {
+    int colonIndex = label.indexOf(':');
+    if (label.startsWith("//") && colonIndex >= 0) {
+      String packageName = label.substring("//".length(), colonIndex);
+      if (!validatePackagePath(packageName, errors)) {
+        return false;
+      }
+      String ruleName = label.substring(colonIndex + 1);
+      if (!RuleName.validate(ruleName, errors)) {
+        return false;
+      }
+      return true;
+    }
+    if (label.startsWith("@") && colonIndex >= 0) {
+      // a bazel-specific label pointing to a different repository
+      int slashIndex = label.indexOf("//");
+      if (slashIndex >= 0) {
+        return validate(label.substring(slashIndex), errors);
+      }
+    }
+    if (errors != null) {
+      errors.add(new BlazeValidationError("Not a valid label, no target name found: " + label));
+    }
+    return false;
+  }
+
+  /**
+   * Extract the rule name from a label. The rule name follows a colon at the end of the label.
+   *
+   * @return the rule name
+   */
+  public RuleName ruleName() {
+    String labelStr = toString();
+    int colonLocation = labelStr.lastIndexOf(':');
+    int ruleNameStart = colonLocation + 1;
+    String ruleNameStr = labelStr.substring(ruleNameStart);
+    return RuleName.create(ruleNameStr);
+  }
+
+  /**
+   * Return the workspace path for the package label for the given label. For example, if the
+   * package is //j/c/g/a/apps/docs:release, it returns j/c/g/a/apps/docs.
+   */
+  public WorkspacePath blazePackage() {
+    String labelStr = toString();
+    int startIndex = labelStr.indexOf("//") + "//".length();
+    int colonIndex = labelStr.lastIndexOf(':');
+    LOG.assertTrue(colonIndex >= 0);
+    return new WorkspacePath(labelStr.substring(startIndex, colonIndex));
+  }
+
+  public static boolean validatePackagePath(String path) {
+    return validatePackagePath(path, null);
+  }
+
+  public static boolean validatePackagePath(String path, @Nullable Collection<BlazeValidationError> errors) {
+    // Empty packages are legal but not recommended
+    if (path.isEmpty()) {
+      return true;
+    }
+
+    if (path.charAt(0) < 'a' || path.charAt(0) > 'z') {
+      BlazeValidationError.collect(errors, new BlazeValidationError(
+        "Invalid package name: " + path + "\n "
+        + "Package names must start with a lowercase ASCII letter"
+      ));
+      return false;
+    }
+    if (path.contains("//")) {
+      BlazeValidationError.collect(errors, new BlazeValidationError(
+        "Invalid package name: " + path + "\n "
+        + "package names may not contain \"//\" path separators."
+      ));
+      return false;
+    }
+    if (path.endsWith("/")) {
+      BlazeValidationError.collect(errors, new BlazeValidationError(
+        "Invalid package name: " + path + "\n "
+        + "package names may not end with \"/\""
+      ));
+      return false;
+    }
+    return true;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.java
new file mode 100644
index 0000000..86cedf7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/LanguageClass.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.base.model.primitives;
+
+/**
+ * Language classes.
+ */
+public enum LanguageClass {
+  GENERIC("generic"),
+  C("c"),
+  JAVA("java"),
+  ANDROID("android"),
+  JAVASCRIPT("javascript"),
+  TYPESCRIPT("typescript"),
+  DART("dart");
+
+  private final String name;
+  LanguageClass(String name) {
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public static LanguageClass fromString(String name) {
+    for (LanguageClass ruleClass : LanguageClass.values()) {
+      if (ruleClass.name.equals(name)) {
+        return ruleClass;
+      }
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/RuleName.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/RuleName.java
new file mode 100644
index 0000000..aed7e88
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/RuleName.java
@@ -0,0 +1,188 @@
+/*
+ * 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.model.primitives;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public final class RuleName {
+
+  // This is a subset of the allowable target names in Blaze
+  private static final String ALNUM_REGEX_STR = "[a-zA-Z0-9]*";
+  private static final Pattern ALNUM_REGEX = Pattern.compile(ALNUM_REGEX_STR);
+
+  // Rule names must be alpha-numeric or consist of the following allowed chars:
+  // (note, rule names can also contain '/'; we handle that case separately)
+  private static final ImmutableSet<Character> ALLOWED_META = ImmutableSet.of('+', '_', ',', '=', '-', '.', '@', '~');
+
+  private final String name;
+
+  private RuleName(String ruleName) {
+    this.name = ruleName;
+  }
+
+  /**
+   * Silently returns null if the string is not a valid rule name.
+   */
+  @Nullable
+  public static RuleName createIfValid(String ruleName) {
+    if (validate(ruleName, null)) {
+      return new RuleName(ruleName);
+    }
+    return null;
+  }
+
+  public static RuleName create(String ruleName) {
+    List<BlazeValidationError> errors = Lists.newArrayList();
+    if (!validate(ruleName, errors)) {
+      BlazeValidationError.throwError(errors);
+    }
+    return new RuleName(ruleName);
+  }
+
+  /**
+   * Validates a rule name using the same logic as Blaze
+   */
+  public static boolean validate(String ruleName) {
+    return validate(ruleName, null);
+  }
+
+  /**
+   * Validates a rule name using the same logic as Blaze
+   */
+  public static boolean validate(String ruleName, @Nullable Collection<BlazeValidationError> errors) {
+    if (ruleName.isEmpty()) {
+      BlazeValidationError.collect(errors, new BlazeValidationError("target names cannot be empty"));
+      return false;
+    }
+    // Forbidden start chars:
+    if (ruleName.charAt(0) == '/') {
+      BlazeValidationError.collect(errors, new BlazeValidationError(
+        "Invalid target name: " + ruleName + "\n" +
+        "target names may not start with \"/\""
+      ));
+      return false;
+    }
+    else if (ruleName.charAt(0) == '.') {
+      if (ruleName.startsWith("../") || ruleName.equals("..")) {
+        BlazeValidationError.collect(errors, new BlazeValidationError(
+          "Invalid target name: " + ruleName + "\n" +
+          "target names may not contain up-level references \"..\""
+        ));
+        return false;
+      }
+      else if (ruleName.equals(".")) {
+        return true;
+      }
+      else if (ruleName.startsWith("./")) {
+        BlazeValidationError.collect(errors, new BlazeValidationError(
+          "Invalid target name: " + ruleName + "\n" +
+          "target names may not contain \".\" as a path segment"
+        ));
+        return false;
+      }
+    }
+
+    for (int i = 0; i < ruleName.length(); ++i) {
+      char c = ruleName.charAt(i);
+      if (ALLOWED_META.contains(c)) {
+        continue;
+      }
+      if (c == '/') {
+        // Forbidden substrings: "/../", "/./", "//"
+        if (ruleName.contains("/../")) {
+          BlazeValidationError.collect(errors, new BlazeValidationError(
+            "Invalid target name: " + ruleName + "\n" +
+            "target names may not contain up-level references \"..\""
+          ));
+          return false;
+        }
+        else if (ruleName.contains("/./")) {
+          BlazeValidationError.collect(errors, new BlazeValidationError(
+            "Invalid target name: " + ruleName + "\n" +
+            "target names may not contain \".\" as a path segment"
+          ));
+          return false;
+        }
+        else if (ruleName.contains("//")) {
+          BlazeValidationError.collect(errors, new BlazeValidationError(
+            "Invalid target name: " + ruleName + "\n" +
+            "target names may not contain \"//\" path separators"
+          ));
+          return false;
+        }
+        continue;
+      }
+      boolean isAlnum = ALNUM_REGEX.matcher(String.valueOf(c)).matches();
+      if (!isAlnum) {
+        BlazeValidationError.collect(errors, new BlazeValidationError(
+          "Invalid target name: " + ruleName + "\n" +
+          "target names may not contain " + c
+        ));
+        return false;
+      }
+    }
+
+    // Forbidden end chars:
+    if (ruleName.endsWith("/..")) {
+      BlazeValidationError.collect(errors, new BlazeValidationError(
+        "Invalid target name: " + ruleName + "\n" +
+        "target names may not contain up-level references \"..\""
+      ));
+      return false;
+    }
+    else if (ruleName.endsWith("/.")) {
+      return true;
+    }
+    else if (ruleName.endsWith("/")) {
+      BlazeValidationError.collect(errors, new BlazeValidationError(
+        "Invalid target name: " + ruleName + "\n" +
+        "target names may not end with \"/\""
+      ));
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(name);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj instanceof RuleName) {
+      RuleName that = (RuleName) obj;
+      return Objects.equal(name, that.name);
+    }
+    return false;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/TargetExpression.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/TargetExpression.java
new file mode 100644
index 0000000..6093869
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/TargetExpression.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.model.primitives;
+
+import com.google.common.base.Preconditions;
+
+import java.io.Serializable;
+
+/**
+ * An interface for objects that represent targets you could pass to Blaze on the command line.
+ * See {@link com.google.idea.blaze.base.model.primitives.Label},
+ */
+public class TargetExpression implements Serializable, Comparable<TargetExpression> {
+  public static final long serialVersionUID = 1L;
+
+  private final String expression;
+
+  /**
+   * @return A Label instance if the expression is a valid label, or a TargetExpression
+   * instance if it is not.
+   */
+  public static TargetExpression fromString(String expression) {
+    return Label.validate(expression)
+           ? new Label(expression)
+           : new TargetExpression(expression);
+  }
+
+  TargetExpression(String expression) {
+    // TODO(joshgiles): Validation/canonicalization for target expressions.
+    // For reference, handled in Blaze/Bazel in TargetPattern.java.
+    Preconditions.checkArgument(!expression.isEmpty(), "Target should be non-empty.");
+    this.expression = expression;
+  }
+
+  @Override
+  public String toString() {
+    return expression;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (!(o instanceof TargetExpression)) {
+      return false;
+    }
+    TargetExpression that = (TargetExpression)o;
+    return expression.equals(that.expression);
+  }
+
+  @Override
+  public int hashCode() {
+    return expression.hashCode();
+  }
+
+  /**
+   * All targets in all packages below the given path
+   */
+  public static TargetExpression allFromPackageRecursive(WorkspacePath localPackage) {
+    if (localPackage.relativePath().isEmpty()) {
+      // localPackage is the workspace root
+      return new TargetExpression("//...:all");
+    }
+    return new TargetExpression("//" + localPackage.relativePath() + "/...:all");
+  }
+
+  public static TargetExpression allFromPackageNonRecursive(WorkspacePath localPackage) {
+    return new TargetExpression("//" + localPackage.relativePath() + ":all");
+  }
+
+  @Override
+  public int compareTo(TargetExpression o) {
+    return expression.compareTo(o.expression);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
new file mode 100644
index 0000000..4a5c60a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspacePath.java
@@ -0,0 +1,121 @@
+/*
+ * 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.model.primitives;
+
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Represents a path relative to the workspace root. The path component separator is Blaze specific.
+ * <p/>
+ * A {@link WorkspacePath} is *not* necessarily a valid package name/path. The primary reason is
+ * because it could represent a file and files don't have to follow the same conventions as package
+ * names.
+ */
+@Immutable
+public class WorkspacePath implements Serializable {
+  public static final long serialVersionUID = 1L;
+
+  /**
+   * Silently returns null if this is not a valid workspace path.
+   */
+  @Nullable
+  public static WorkspacePath createIfValid(String relativePath) {
+    if (validate(relativePath)) {
+      return new WorkspacePath(relativePath);
+    }
+    return null;
+  }
+
+  private static final char BLAZE_COMPONENT_SEPARATOR = '/';
+
+  @NotNull
+  private final String relativePath;
+
+  /**
+   * @param relativePath relative path that must use the Blaze specific separator char to separate
+   *                     path components
+   */
+  public WorkspacePath(@NotNull String relativePath) {
+    if (!validate(relativePath)) {
+      throw new IllegalArgumentException("Invalid workspace path: " + relativePath);
+    }
+    this.relativePath = relativePath;
+  }
+
+  public WorkspacePath(@NotNull WorkspacePath parentPath, @NotNull String childPath) {
+    this(parentPath.relativePath() + BLAZE_COMPONENT_SEPARATOR + childPath);
+  }
+
+  public static boolean validate(@NotNull String relativePath) {
+    return validate(relativePath, null);
+  }
+
+  public static boolean validate(@NotNull String relativePath, @Nullable Collection<BlazeValidationError> errors) {
+    if (relativePath.startsWith("/") ) {
+      BlazeValidationError.collect(errors, new BlazeValidationError("Workspace path may not start with '/': " + relativePath));
+      return false;
+    }
+
+    if (relativePath.endsWith("/") ) {
+      BlazeValidationError.collect(errors, new BlazeValidationError("Workspace path may not end with '/': " + relativePath));
+      return false;
+    }
+
+    if (relativePath.indexOf(':') >= 0) {
+      BlazeValidationError.collect(errors, new BlazeValidationError("Workspace path may not contain ':': " + relativePath));
+      return false;
+    }
+
+    return true;
+  }
+
+  public boolean isWorkspaceRoot() {
+    return relativePath.isEmpty();
+  }
+
+  @Override
+  public String toString() {
+    return relativePath;
+  }
+
+  public String relativePath() {
+    return relativePath;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || this.getClass() != o.getClass()) {
+      return false;
+    }
+
+    WorkspacePath that = (WorkspacePath)o;
+    return relativePath.equals(that.relativePath);
+  }
+
+  @Override
+  public int hashCode() {
+    return relativePath.hashCode();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspaceRoot.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspaceRoot.java
new file mode 100644
index 0000000..917603a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspaceRoot.java
@@ -0,0 +1,136 @@
+/*
+ * 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.model.primitives;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.Serializable;
+
+/**
+ * Represents a workspace root
+ */
+public class WorkspaceRoot implements Serializable {
+  public static final long serialVersionUID = 1L;
+
+  private final File directory;
+
+  public WorkspaceRoot(File directory) {
+    this.directory = directory;
+  }
+
+  /**
+   * Get the workspace root for a project
+   *
+   * @param blazeSettings settings for the project in question
+   * @return the path to workspace root that is used for the project
+   */
+  public static WorkspaceRoot fromImportSettings(BlazeImportSettings blazeSettings) {
+    return new WorkspaceRoot(new File(blazeSettings.getWorkspaceRoot()));
+  }
+
+  /**
+   * Tries to load the import settings for the given project and get the workspace root directory.<br>
+   * Unlike {@link #fromProject}, it will silently return null if this is not a blaze project.
+   */
+  @Nullable
+  public static WorkspaceRoot fromProjectSafe(Project project) {
+    if (Blaze.isBlazeProject(project)) {
+      return fromProject(project);
+    }
+    return null;
+  }
+
+  /**
+   * Tries to load the import settings for the given project and get the workspace root directory.
+   */
+  public static WorkspaceRoot fromProject(Project project) {
+    BlazeImportSettings importSettings = BlazeImportSettingsManager.getInstance(project)
+      .getImportSettings();
+    if (importSettings == null) {
+      throw new IllegalStateException("null BlazeImportSettings.");
+    }
+    return fromImportSettings(importSettings);
+  }
+
+  public File fileForPath(WorkspacePath workspacePath) {
+    return new File(directory, workspacePath.relativePath());
+  }
+
+  public File directory() {
+    return directory;
+  }
+
+  public WorkspacePath workspacePathFor(VirtualFile file) {
+    return workspacePathFor(file.getPath());
+  }
+
+  public boolean isInWorkspace(VirtualFile file) {
+    return isInWorkspace(file.getPath());
+  }
+
+  public WorkspacePath workspacePathFor(File file) {
+    return workspacePathFor(file.getPath());
+  }
+
+  public boolean isInWorkspace(File file) {
+    return isInWorkspace(file.getPath());
+  }
+
+  private WorkspacePath workspacePathFor(String path) {
+    if (!isInWorkspace(path)) {
+      throw new IllegalArgumentException("File is not under this workspace");
+    }
+    if (directory.getPath().length() == path.length()) {
+      return new WorkspacePath("");
+    }
+    return new WorkspacePath(path.substring(directory.getPath().length() + 1));
+  }
+
+  private boolean isInWorkspace(String path) {
+    return FileUtil.isAncestor(directory.getPath(), path, false);
+  }
+
+  @Override
+  public String toString() {
+    return directory.toString();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    WorkspaceRoot that = (WorkspaceRoot)o;
+    return directory.equals(that.directory);
+  }
+
+  @Override
+  public int hashCode() {
+    return directory.hashCode();
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java b/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java
new file mode 100644
index 0000000..fbd7eb1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/model/primitives/WorkspaceType.java
@@ -0,0 +1,60 @@
+/*
+ * 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.model.primitives;
+
+/**
+ * Workspace types.
+ *
+ * <p>If the user doesn't specify a workspace, she gets the highest
+ * supported workspace type by enum ordinal.
+ */
+public enum WorkspaceType {
+  INTELLIJ_PLUGIN("intellij_plugin", LanguageClass.JAVA),
+  C("c", LanguageClass.C),
+  JAVA("java", LanguageClass.JAVA),
+  ANDROID_NDK("android_ndk", LanguageClass.ANDROID, LanguageClass.JAVA, LanguageClass.C),
+  ANDROID("android", LanguageClass.ANDROID, LanguageClass.JAVA),
+  JAVASCRIPT("javascript");
+
+  private final String name;
+  private final LanguageClass[] languages;
+  WorkspaceType(String name, LanguageClass... languages) {
+    this.name = name;
+    this.languages = languages;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public LanguageClass[] getLanguages() {
+    return languages;
+  }
+
+  public static WorkspaceType fromString(String name) {
+    for (WorkspaceType ruleClass : WorkspaceType.values()) {
+      if (ruleClass.name.equals(name)) {
+        return ruleClass;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeActionRemover.java b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeActionRemover.java
new file mode 100644
index 0000000..ebb3fc0
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeActionRemover.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.plugin;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Wraps an action and makes it invisible for blaze-based projects.
+ */
+public class BlazeActionRemover extends AnAction {
+
+  public static void hideAction(String actionId) {
+    AnAction oldAction = ActionManager.getInstance().getAction(actionId);
+    if (oldAction != null) {
+      replaceAction(actionId, new BlazeActionRemover(oldAction));
+    }
+  }
+
+  private static void replaceAction(String actionId, AnAction newAction) {
+    ActionManager actionManager = ActionManager.getInstance();
+    AnAction oldAction = actionManager.getAction(actionId);
+    if (oldAction != null) {
+      newAction.getTemplatePresentation().setIcon(oldAction.getTemplatePresentation().getIcon());
+      actionManager.unregisterAction(actionId);
+    }
+    actionManager.registerAction(actionId, newAction);
+  }
+
+  private final AnAction delegate;
+
+  private BlazeActionRemover(AnAction delegate) {
+    super(delegate.getTemplatePresentation().getTextWithMnemonic(),
+          delegate.getTemplatePresentation().getDescription(),
+          delegate.getTemplatePresentation().getIcon());
+    this.delegate = delegate;
+  }
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    delegate.actionPerformed(e);
+  }
+
+  @Override
+  public void update(AnActionEvent e) {
+    Presentation presentation = e.getPresentation();
+    Project project = e.getProject();
+    if (project != null && Blaze.isBlazeProject(project)) {
+      presentation.setEnabledAndVisible(false);
+      return;
+    }
+    delegate.update(e);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeBinaryFileType.java b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeBinaryFileType.java
new file mode 100644
index 0000000..d032499
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeBinaryFileType.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.plugin;
+
+import com.intellij.openapi.fileTypes.UserBinaryFileType;
+
+/**
+ * Marker type to mark something as binary.
+ */
+public class BlazeBinaryFileType extends UserBinaryFileType {
+  public static final BlazeBinaryFileType INSTANCE;
+  static {
+    INSTANCE = new BlazeBinaryFileType();
+    INSTANCE.setName("Binary File");
+    INSTANCE.setDescription("The blaze plugin has guessed this file type as binary for performance.");
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeFileTypeFactory.java b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeFileTypeFactory.java
new file mode 100644
index 0000000..03d8771
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeFileTypeFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.plugin;
+
+import com.intellij.ide.highlighter.ArchiveFileType;
+import com.intellij.openapi.fileTypes.FileTypeConsumer;
+import com.intellij.openapi.fileTypes.FileTypeFactory;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * @author chuckj
+ */
+public class BlazeFileTypeFactory extends FileTypeFactory {
+  @Override
+  public void createFileTypes(@NotNull final FileTypeConsumer consumer) {
+    consumer.consume(ArchiveFileType.INSTANCE, "srcjar");
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/plugin/BlazePluginId.java b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazePluginId.java
new file mode 100644
index 0000000..4ef6f93
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazePluginId.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.plugin;
+
+import com.intellij.openapi.components.ServiceManager;
+
+/**
+ * Supplies the ID of the sync plugin.
+ */
+public interface BlazePluginId {
+
+  static BlazePluginId getInstance() {
+    return ServiceManager.getService(BlazePluginId.class);
+  }
+
+  /**
+   * @return the plugin ID (same as in the plugin.xml).
+   */
+  String getPluginId();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeSpecificInitializer.java b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeSpecificInitializer.java
new file mode 100644
index 0000000..1d1e4f8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/plugin/BlazeSpecificInitializer.java
@@ -0,0 +1,53 @@
+/*
+ * 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.plugin;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileTypeFactory;
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.openapi.components.ApplicationComponent;
+
+/**
+ * Runs on startup.
+ */
+public class BlazeSpecificInitializer extends ApplicationComponent.Adapter {
+
+  @Override
+  public void initComponent() {
+    hideMakeActions();
+    initializeBuildFileSupportStatus();
+  }
+
+  private static void initializeBuildFileSupportStatus() {
+    BuildFileTypeFactory.updateBuildFileLanguageEnabled(BuildFileLanguage.buildFileSupportEnabled());
+  }
+
+  // The original actions will be visible only on plain IDEA projects.
+  private static void hideMakeActions() {
+    // 'Build' > 'Make Project' action
+    BlazeActionRemover.hideAction("CompileDirty");
+
+    // 'Build' > 'Make Modules' action
+    BlazeActionRemover.hideAction(IdeActions.ACTION_MAKE_MODULE);
+
+    // 'Build' > 'Rebuild' action
+    BlazeActionRemover.hideAction(IdeActions.ACTION_COMPILE_PROJECT);
+
+    // 'Build' > 'Compile Modules' action
+    BlazeActionRemover.hideAction(IdeActions.ACTION_COMPILE);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/plugin/Version.java b/blaze-base/src/com/google/idea/blaze/base/plugin/Version.java
new file mode 100644
index 0000000..5806517
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/plugin/Version.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.plugin;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.openapi.extensions.PluginId;
+
+/**
+ * Blaze sync plugin ID and version information.
+ */
+public class Version {
+
+  public static class PluginInfo {
+    public final String id;
+    public final String version;
+
+    @VisibleForTesting
+    public static final PluginInfo UNKNOWN = new PluginInfo("UNKNOWN_PLUGIN", "UNKNOWN_VERSION");
+
+    public PluginInfo(String id, String version) {
+      this.id = id;
+      this.version = version;
+    }
+  }
+
+  public static PluginInfo getSyncPluginInfo() {
+    BlazePluginId idService = BlazePluginId.getInstance();
+    if (idService != null) {
+      PluginId pluginId = PluginId.getId(idService.getPluginId());
+      IdeaPluginDescriptor pluginInfo = PluginManager.getPlugin(pluginId);
+      if (pluginInfo != null) {
+        return new PluginInfo(pluginId.getIdString(), pluginInfo.getVersion());
+      }
+    }
+    return PluginInfo.UNKNOWN;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/plugin/dependency/PluginDependencyHelper.java b/blaze-base/src/com/google/idea/blaze/base/plugin/dependency/PluginDependencyHelper.java
new file mode 100644
index 0000000..0e4bac1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/plugin/dependency/PluginDependencyHelper.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.plugin.dependency;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.plugin.BlazePluginId;
+import com.intellij.externalDependencies.DependencyOnPlugin;
+import com.intellij.externalDependencies.ExternalDependenciesManager;
+import com.intellij.externalDependencies.ProjectExternalDependency;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+public class PluginDependencyHelper {
+
+  public static void addDependencyOnSyncPlugin(@NotNull Project blazeProject) {
+    BlazePluginId idService = BlazePluginId.getInstance();
+    if (idService != null) {
+      addDependency(
+        blazeProject,
+        new DependencyOnPlugin(idService.getPluginId(), null, null, null)
+      );
+    }
+  }
+
+  /**
+   * Adds dependency, or replaces existing dependency of same type.
+   * Doesn't trigger any update checking
+   */
+  private static void addDependency(
+    @NotNull Project project,
+    @NotNull DependencyOnPlugin newDep) {
+
+    ExternalDependenciesManager manager = ExternalDependenciesManager.getInstance(project);
+    List<ProjectExternalDependency> deps = Lists.newArrayList(manager.getAllDependencies());
+    boolean added = false;
+    for (int i = 0; i < deps.size(); i++) {
+      ProjectExternalDependency dep = deps.get(i);
+      if (!(dep instanceof DependencyOnPlugin)) {
+        continue;
+      }
+      DependencyOnPlugin pluginDep = (DependencyOnPlugin) dep;
+      if (pluginDep.getPluginId().equals(newDep.getPluginId())) {
+        added = true;
+        deps.set(i, newDep);
+      }
+    }
+    if (!added) {
+      deps.add(newDep);
+    }
+    manager.setAllDependencies(deps);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/plugin/dependency/ProjectDependencyMigration.java b/blaze-base/src/com/google/idea/blaze/base/plugin/dependency/ProjectDependencyMigration.java
new file mode 100644
index 0000000..eafdbf9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/plugin/dependency/ProjectDependencyMigration.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.plugin.dependency;
+
+import com.google.idea.blaze.base.settings.Blaze;
+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. Listens for blaze projects opening and closing,
+ * and adds required plugin dependencies
+ */
+public class ProjectDependencyMigration 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)) {
+          PluginDependencyHelper.addDependencyOnSyncPlugin(project);
+        }
+      }
+    });
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java b/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchService.java
new file mode 100644
index 0000000..fa7594c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchService.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.base.prefetch;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Interface to request prefetching of files
+ */
+public interface PrefetchService {
+  static PrefetchService getInstance() {
+    return ServiceManager.getService(PrefetchService.class);
+  }
+
+  /**
+   * Instructs all prefetchers to prefetch these files.
+   *
+   * @param files The files to prefetch
+   * @param synchronous A hint whether the prefetch should be complete when the future completes.
+   */
+  ListenableFuture<?> prefetchFiles(List<File> files, boolean synchronous);
+
+  /**
+   * Instructs all prefetchers to prefetch any project files they're interested in.
+   *
+   * Should be asynchronous.
+   */
+  void prefetchProjectFiles(Project project);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java b/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java
new file mode 100644
index 0000000..a335bb6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceImpl.java
@@ -0,0 +1,53 @@
+/*
+ * 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.prefetch;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.concurrency.BoundedTaskExecutor;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.Executors;
+
+/**
+ * Implementation for prefetcher.
+ */
+public class PrefetchServiceImpl implements PrefetchService {
+  private static final int THREAD_COUNT = 32;
+  private final ListeningExecutorService executor = MoreExecutors.listeningDecorator(
+    new BoundedTaskExecutor(Executors.newFixedThreadPool(THREAD_COUNT), THREAD_COUNT));
+
+  @Override
+  public ListenableFuture<?> prefetchFiles(List<File> files, boolean synchronous) {
+    List<ListenableFuture<?>> futures = Lists.newArrayList();
+    for (Prefetcher prefetcher : Prefetcher.EP_NAME.getExtensions()) {
+      futures.add(prefetcher.prefetchFiles(files, executor, synchronous));
+    }
+    return Futures.allAsList(futures);
+  }
+
+  @Override
+  public void prefetchProjectFiles(Project project) {
+    for (Prefetcher prefetcher : Prefetcher.EP_NAME.getExtensions()) {
+      prefetcher.prefetchProjectFiles(project, executor);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceProjectComponent.java b/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceProjectComponent.java
new file mode 100644
index 0000000..66a404e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/prefetch/PrefetchServiceProjectComponent.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.prefetch;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.ProjectTopics;
+import com.intellij.openapi.components.ProjectComponent;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModuleRootAdapter;
+import com.intellij.openapi.roots.ModuleRootEvent;
+import com.intellij.util.messages.MessageBusConnection;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Instructs prefetchers to prefetch all project files when they may have changed / be stale.
+ */
+public class PrefetchServiceProjectComponent implements ProjectComponent {
+  private final Project project;
+
+  public PrefetchServiceProjectComponent(Project project) {
+    this.project = project;
+    MessageBusConnection connection = project.getMessageBus().connect(project);
+    connection.subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootAdapter() {
+      @Override
+      public void rootsChanged(ModuleRootEvent event) {
+        prefetch();
+      }
+    });
+  }
+
+  @Override
+  public void projectOpened() {
+    prefetch();
+  }
+
+  private void prefetch() {
+    if (!Blaze.isBlazeProject(project)) {
+      return;
+    }
+    PrefetchService.getInstance().prefetchProjectFiles(project);
+  }
+
+  @Override
+  public void projectClosed() {
+  }
+
+  @Override
+  public void initComponent() {
+  }
+
+  @Override
+  public void disposeComponent() {
+  }
+
+  @NotNull
+  @Override
+  public String getComponentName() {
+    return "PrefetchServiceProjectComponent";
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/prefetch/Prefetcher.java b/blaze-base/src/com/google/idea/blaze/base/prefetch/Prefetcher.java
new file mode 100644
index 0000000..274b2b5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/prefetch/Prefetcher.java
@@ -0,0 +1,48 @@
+/*
+ * 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.prefetch;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+
+import java.io.File;
+
+/**
+ * Prefetches files when a project is opened or roots change.
+ */
+public interface Prefetcher {
+  ExtensionPointName<Prefetcher> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.Prefetcher");
+
+  /**
+   * Prefetches the given list of files.
+   *
+   * It is the responsibility of the prefetcher to filter out any files it isn't interested in.
+   *
+   * @param synchronous A hint whether the prefetch should be complete when the returned future completes
+   */
+  ListenableFuture<?> prefetchFiles(Iterable<File> file,
+                                    ListeningExecutorService executor,
+                                    boolean synchrononous);
+
+  /**
+   * Prefetch any project files that this prefetcher is interested in.
+   *
+   * <p>The prefetch should be asynchronous.
+   */
+  void prefetchProjectFiles(Project project, ListeningExecutorService executor);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectView.java b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectView.java
new file mode 100644
index 0000000..7366b6b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectView.java
@@ -0,0 +1,90 @@
+/*
+ * 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.projectview;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.projectview.section.Section;
+import com.google.idea.blaze.base.projectview.section.SectionBuilder;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.intellij.openapi.diagnostic.Logger;
+
+import javax.annotation.Nullable;
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * Represents instructions for what should be included in a project.
+ */
+public final class ProjectView implements Serializable {
+  private static final long serialVersionUID = 2L;
+
+  private static final Logger LOG = Logger.getInstance(ProjectView.class);
+  private final ImmutableMap<SectionKey, Section> sections;
+
+  private ProjectView(ImmutableMap<SectionKey, Section> sections) {
+    this.sections = sections;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Nullable
+  public <T, SectionType extends Section<T>> SectionType getSectionOfType(SectionKey<T, SectionType> key) {
+    return (SectionType) sections.get(key);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static Builder builder(ProjectView projectView) {
+    return new Builder(projectView);
+  }
+
+  /**
+   * Builder class.
+   */
+  public static class Builder {
+    private final Map<SectionKey, Section> sections;
+
+    Builder() {
+      sections = Maps.newHashMap();
+    }
+
+    Builder(ProjectView projectView) {
+      sections = Maps.newHashMap(projectView.sections);
+    }
+
+    public <T, SectionType extends Section<T>> Builder put(SectionBuilder<T, SectionType> builder) {
+      sections.put(builder.getSectionKey(), builder.build());
+      return this;
+    }
+
+    public <T, SectionType extends Section<T>> Builder put(SectionKey<T, SectionType> key, SectionType section) {
+      sections.put(key, section);
+      return this;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Nullable
+    public <T, SectionType extends Section<T>> SectionType get(SectionKey<T, SectionType> key) {
+      return (SectionType) sections.get(key);
+    }
+
+    public ProjectView build() {
+      return new ProjectView(ImmutableMap.copyOf(sections));
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewEdit.java b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewEdit.java
new file mode 100644
index 0000000..ea647f8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewEdit.java
@@ -0,0 +1,123 @@
+/*
+ * 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.projectview;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Represents a modification to one or more project view files.
+ */
+public class ProjectViewEdit {
+
+  private static final Logger LOG = Logger.getInstance(ProjectViewEdit.class);
+  private final Project project;
+  private final List<Modification> modifications;
+
+  ProjectViewEdit(Project project, List<Modification> modifications) {
+    this.project = project;
+    this.modifications = modifications;
+  }
+
+  private static class Modification {
+    ProjectView oldProjectView;
+    ProjectView newProjectView;
+    File projectViewFile;
+  }
+
+  /**
+   * Creates a new edit that modifies all reachable project views in the project view set.
+   */
+  public static ProjectViewEdit editProjectViewSet(Project project, ProjectViewEditor editor) {
+    ProjectViewSet oldProjectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    List<Modification> modifications = Lists.newArrayList();
+    if (oldProjectViewSet != null) {
+      for (ProjectViewSet.ProjectViewFile projectViewFile : oldProjectViewSet.getProjectViewFiles()) {
+        ProjectView.Builder builder = ProjectView.builder(projectViewFile.projectView);
+        if (editor.editProjectView(builder)) {
+          Modification modification = new Modification();
+          modification.newProjectView = builder.build();
+          modification.oldProjectView = projectViewFile.projectView;
+          modification.projectViewFile = projectViewFile.projectViewFile;
+          modifications.add(modification);
+        }
+      }
+    }
+    return new ProjectViewEdit(project, modifications);
+  }
+
+  /**
+   * Creates a new edit that modifies the local project view only.
+   */
+  public static ProjectViewEdit editLocalProjectView(Project project, ProjectViewEditor editor) {
+    List<Modification> modifications = Lists.newArrayList();
+    ProjectViewSet oldProjectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (oldProjectViewSet != null) {
+      ProjectViewSet.ProjectViewFile projectViewFile = oldProjectViewSet.getTopLevelProjectViewFile();
+      if (projectViewFile != null) {
+        ProjectView.Builder builder = ProjectView.builder(projectViewFile.projectView);
+        if (editor.editProjectView(builder)) {
+          Modification modification = new Modification();
+          modification.newProjectView = builder.build();
+          modification.oldProjectView = projectViewFile.projectView;
+          modification.projectViewFile = projectViewFile.projectViewFile;
+          modifications.add(modification);
+        }
+      }
+    }
+    return new ProjectViewEdit(project, modifications);
+  }
+
+  public void apply() {
+    apply(true);
+  }
+
+  public void undo() {
+    apply(false);
+  }
+
+  private void apply(boolean isApply) {
+    for (Modification modification : modifications) {
+      ProjectView projectView = isApply ? modification.newProjectView : modification.oldProjectView;
+      String projectViewText = ProjectViewParser.projectViewToString(projectView);
+      try {
+        ProjectViewStorageManager.getInstance().writeProjectView(projectViewText, modification.projectViewFile);
+      }
+      catch (IOException e) {
+        LOG.error(e);
+        Messages.showErrorDialog(
+          project,
+          "Could not write updated project view. Is the file write protected?",
+          "Edit Failed");
+      }
+    }
+  }
+
+  public boolean hasModifications() {
+    return !modifications.isEmpty();
+  }
+
+  public interface ProjectViewEditor {
+    boolean editProjectView(ProjectView.Builder builder);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewManager.java b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewManager.java
new file mode 100644
index 0000000..7f24cdb
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewManager.java
@@ -0,0 +1,48 @@
+/*
+ * 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.projectview;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+/**
+ * Class that manages access to a project's
+ * {@link ProjectView}.
+ */
+public abstract class ProjectViewManager {
+
+  public static ProjectViewManager getInstance(Project project) {
+    return ServiceManager.getService(project, ProjectViewManager.class);
+  }
+
+  /**
+   * Returns the current project view collection. If there is an error, returns null.
+   */
+  @Nullable
+  public abstract ProjectViewSet getProjectViewSet();
+
+  /**
+   * Reloads the project view, replacing the current one only if there are no errors.
+   *
+   * @return Success.
+   */
+  @Nullable
+  public abstract ProjectViewSet reloadProjectView(BlazeContext context, WorkspacePathResolver workspacePathResolver);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java
new file mode 100644
index 0000000..c497b70
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewManagerImpl.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.data.BlazeDataStorage;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.util.SerializationUtil;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Project view manager implementation.
+ */
+/**
+ * Stores mutable per-project user settings.
+ */
+final class ProjectViewManagerImpl extends ProjectViewManager {
+
+  private static final Logger LOG = Logger.getInstance(ProjectViewManagerImpl.class);
+  private static final String CACHE_FILE_NAME = "project.view.dat";
+
+  private final Project project;
+  @Nullable private ProjectViewSet projectViewSet;
+  private boolean projectViewSetLoaded = false;
+
+  public ProjectViewManagerImpl(@NotNull Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public ProjectViewSet getProjectViewSet() {
+    if (projectViewSet == null && !projectViewSetLoaded) {
+      ProjectViewSet loadedProjectViewSet = null;
+      try {
+        BlazeImportSettings importSettings = BlazeImportSettingsManager.getInstance(project).getImportSettings();
+        if (importSettings == null) {
+          return null;
+        }
+        File file = getCacheFile(project, importSettings);
+
+        List<ClassLoader> classLoaders = Lists.newArrayList();
+        classLoaders.add(getClass().getClassLoader());
+        classLoaders.add(Thread.currentThread().getContextClassLoader());
+        loadedProjectViewSet = (ProjectViewSet) SerializationUtil.loadFromDisk(file, classLoaders);
+      } catch (IOException e) {
+        LOG.info(e);
+      }
+      this.projectViewSet = loadedProjectViewSet;
+      this.projectViewSetLoaded = true;
+    }
+    return projectViewSet;
+  }
+
+  @Override
+  public ProjectViewSet reloadProjectView(BlazeContext context, WorkspacePathResolver workspacePathResolver) {
+    BlazeImportSettings importSettings = BlazeImportSettingsManager.getInstance(project).getImportSettings();
+    assert importSettings != null;
+    assert importSettings.getProjectViewFile() != null;
+    File projectViewFile = new File(importSettings.getProjectViewFile());
+    ProjectViewParser parser = new ProjectViewParser(context, workspacePathResolver);
+    parser.parseProjectView(projectViewFile);
+
+    boolean success = !context.hasErrors();
+    if (success) {
+      ProjectViewSet projectViewSet = parser.getResult();
+      File file = getCacheFile(project, importSettings);
+      try {
+        SerializationUtil.saveToDisk(file, projectViewSet);
+      }
+      catch (IOException e) {
+        LOG.error(e);
+      }
+      this.projectViewSet = projectViewSet;
+    }
+    return success ? projectViewSet : null;
+  }
+
+  private static File getCacheFile(Project project, BlazeImportSettings importSettings) {
+    return new File(BlazeDataStorage.getProjectCacheDir(project, importSettings), CACHE_FILE_NAME);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.java
new file mode 100644
index 0000000..adcfaa4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewSet.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.projectview;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.Section;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A collection of project views and their file names.
+ */
+public final class ProjectViewSet implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  private final ImmutableList<ProjectViewFile> projectViewFiles;
+
+  public ProjectViewSet(ImmutableList<ProjectViewFile> projectViewFiles) {
+    this.projectViewFiles = projectViewFiles;
+  }
+
+  public <T> List<T> listItems(SectionKey<T, ListSection<T>> key) {
+    List<T> result = Lists.newArrayList();
+    for (ListSection<T> section : getSections(key)) {
+      result.addAll(section.items());
+    }
+    return result;
+  }
+
+  @Nullable
+  public <T> T getSectionValue(SectionKey<T, ScalarSection<T>> key) {
+    return getSectionValue(key, null);
+  }
+
+  public <T> T getSectionValue(SectionKey<T, ScalarSection<T>> key, T defaultValue) {
+    Collection<ScalarSection<T>> sections = getSections(key);
+    if (sections.isEmpty()) {
+      return defaultValue;
+    } else {
+      return Iterables.getLast(sections).getValue();
+    }
+  }
+
+  public <T, SectionType extends Section<T>> Collection<SectionType> getSections(SectionKey<T, SectionType> key) {
+    List<SectionType> result = Lists.newArrayList();
+    for (ProjectViewFile projectViewFile : projectViewFiles) {
+      ProjectView projectView = projectViewFile.projectView;
+      SectionType section = projectView.getSectionOfType(key);
+      if (section != null) {
+        result.add(section);
+      }
+    }
+    return result;
+  }
+
+  public Collection<ProjectViewFile> getProjectViewFiles() {
+    return projectViewFiles;
+  }
+
+  @Nullable
+  public ProjectViewFile getTopLevelProjectViewFile() {
+    return !projectViewFiles.isEmpty() ? projectViewFiles.get(projectViewFiles.size() - 1) : null;
+  }
+
+  /**
+   * A project view/file pair
+   */
+  public static class ProjectViewFile implements Serializable {
+    private static final long serialVersionUID = 1L;
+    public final ProjectView projectView;
+    @Nullable public final File projectViewFile;
+
+    public ProjectViewFile(ProjectView projectView, @Nullable File projectViewFile) {
+      this.projectView = projectView;
+      this.projectViewFile = projectViewFile;
+    }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    ImmutableList.Builder<ProjectViewFile> projectViewFiles = ImmutableList.builder();
+
+    public Builder add(ProjectView projectView) {
+      return add(null, projectView);
+    }
+
+    public Builder add(@Nullable File projectViewFile, ProjectView projectView) {
+      projectViewFiles.add(new ProjectViewFile(projectView, projectViewFile));
+      return this;
+    }
+
+    public Builder addAll(Collection<ProjectViewFile> projectViewFiles) {
+      this.projectViewFiles.addAll(projectViewFiles);
+      return this;
+    }
+
+    public ProjectViewSet build() {
+      return new ProjectViewSet(projectViewFiles.build());
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManager.java b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManager.java
new file mode 100644
index 0000000..567db81
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManager.java
@@ -0,0 +1,82 @@
+/*
+ * 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.projectview;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.components.ServiceManager;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Manages project view storage.
+ *
+ * For the most part, use ProjectViewManager instead. This is a lower-level API intended for use by
+ * ProjectViewManager itself, and during the import process before a project exists.
+ */
+public abstract class ProjectViewStorageManager {
+
+  private static final String BLAZE_EXTENSION = "blazeproject";
+  private static final String BAZEL_EXTENSION = "bazelproject";
+  private static final String LEGACY_EXTENSION = "asproject";
+
+  public static final ImmutableList<String> VALID_EXTENSIONS = ImmutableList.of(
+    BLAZE_EXTENSION,
+    BAZEL_EXTENSION,
+    LEGACY_EXTENSION
+  );
+
+  public static boolean isProjectViewFile(@NotNull File file) {
+    return isProjectViewFile(file.getName());
+  }
+
+  public static boolean isProjectViewFile(String fileName) {
+    for (String ext : VALID_EXTENSIONS) {
+      if (fileName.endsWith("." + ext)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static String getProjectViewFileName(BuildSystem buildSystem) {
+    switch (buildSystem) {
+      case Blaze:
+        return "." + BLAZE_EXTENSION;
+      case Bazel:
+        return "." + BAZEL_EXTENSION;
+      default:
+        throw new IllegalArgumentException("Unrecognized build system type: " + buildSystem);
+    }
+  }
+
+  public static File getLocalProjectViewFileName(BuildSystem buildSystem, File projectDataDirectory) {
+    return new File(projectDataDirectory, getProjectViewFileName(buildSystem));
+  }
+
+  public static ProjectViewStorageManager getInstance() {
+    return ServiceManager.getService(ProjectViewStorageManager.class);
+  }
+
+  @Nullable
+  public abstract String loadProjectView(File projectViewFile) throws IOException;
+
+  public abstract void writeProjectView(String projectViewText, File projectViewFile) throws IOException;
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java
new file mode 100644
index 0000000..9f4bed2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewStorageManagerImpl.java
@@ -0,0 +1,59 @@
+/*
+ * 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.projectview;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+
+/**
+ * Project view storage implementation.
+ */
+final class ProjectViewStorageManagerImpl extends ProjectViewStorageManager {
+  private static final Logger LOG = Logger.getInstance(ProjectViewManagerImpl.class);
+
+  @Nullable
+  @Override
+  public String loadProjectView(@NotNull File projectViewFile) throws IOException {
+    FileInputStream fis = new FileInputStream(projectViewFile);
+    byte[] data = new byte[(int)projectViewFile.length()];
+    fis.read(data);
+    fis.close();
+    return new String(data, Charsets.UTF_8);
+  }
+
+  @Override
+  public void writeProjectView(
+    @NotNull String projectViewText,
+    @NotNull File projectViewFile) throws IOException {
+    FileWriter fileWriter = new FileWriter(projectViewFile);
+    try {
+      fileWriter.write(projectViewText);
+    } finally {
+      fileWriter.close();
+    }
+
+    LocalFileSystem.getInstance().refreshIoFiles(ImmutableList.of(projectViewFile));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java
new file mode 100644
index 0000000..705d832
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/ProjectViewVerifier.java
@@ -0,0 +1,201 @@
+/*
+ * 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.projectview;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.io.WorkspaceScanner;
+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.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.projectview.section.sections.ExcludedSourceSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.openapi.util.io.FileUtil;
+
+import java.util.List;
+
+/**
+ * Verifies project views.
+ */
+public class ProjectViewVerifier {
+
+  public static class MissingDirectoryIssueData extends IssueOutput.IssueData {
+    public final WorkspacePath workspacePath;
+    public MissingDirectoryIssueData(WorkspacePath workspacePath) {
+      this.workspacePath = workspacePath;
+    }
+  }
+
+  /**
+   * Verifies the project view. Any errors are output to the context as issues.
+   */
+  public static boolean verifyProjectView(
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet,
+    WorkspaceLanguageSettings workspaceLanguageSettings) {
+    if (!verifyIncludedPackagesExistOnDisk(context, workspaceRoot, projectViewSet)) {
+      return false;
+    }
+    if (!verifyDirectoriesAreNonOverlapping(context, projectViewSet)) {
+      return false;
+    }
+    if (!verifyIncludedPackagesAreNotExcluded(context, projectViewSet)) {
+      return false;
+    }
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      if (!syncPlugin.validateProjectView(context, projectViewSet, workspaceLanguageSettings)) {
+        return false;
+      }
+    }
+    if (!projectViewSet.listItems(ExcludedSourceSection.KEY).isEmpty()) {
+      IssueOutput
+        .warn("excluded_sources is deprecated and has no effect.")
+        .inFile(projectViewSet.getTopLevelProjectViewFile().projectViewFile)
+        .submit(context);
+    }
+    return true;
+  }
+
+  private static boolean verifyDirectoriesAreNonOverlapping(
+    BlazeContext context,
+    ProjectViewSet projectViewSet) {
+    boolean ok = true;
+
+    List<WorkspacePath> includedDirectories = getIncludedDirectories(projectViewSet);
+
+    for (WorkspacePath includedDirectory : includedDirectories) {
+      for (ProjectViewSet.ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
+        ListSection<DirectoryEntry> directorySection = projectViewFile.projectView.getSectionOfType(DirectorySection.KEY);
+        if (directorySection == null) {
+          continue;
+        }
+
+        for (DirectoryEntry entry : directorySection.items()) {
+          if (!entry.included) {
+            continue;
+          }
+
+          if (isAncestor(includedDirectory.relativePath(), entry.directory.relativePath())) {
+            IssueOutput
+              .error(String.format("Overlapping directories: %s already included by %s",
+                                   entry.directory.toString(),
+                                   includedDirectory.toString()))
+              .inFile(projectViewFile.projectViewFile)
+              .submit(context);
+            ok = false;
+          }
+        }
+      }
+    }
+    return ok;
+  }
+
+  /**
+   * Returns true if 'path' is a strict child of 'ancestorPath'.
+   */
+  private static boolean isAncestor(String ancestorPath, String path) {
+    // FileUtil.isAncestor has a bug in its handling of equal, empty paths (it ignores the 'strict' flag in this case).
+    if (ancestorPath.equals(path)) {
+      return false;
+    }
+    return FileUtil.isAncestor(ancestorPath, path, true);
+  }
+
+  private static boolean verifyIncludedPackagesAreNotExcluded(
+    BlazeContext context,
+    ProjectViewSet projectViewSet) {
+    boolean ok = true;
+
+    List<WorkspacePath> includedDirectories = getIncludedDirectories(projectViewSet);
+
+    for (WorkspacePath includedDirectory : includedDirectories) {
+      for (ProjectViewSet.ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
+        ListSection<DirectoryEntry> directorySection = projectViewFile.projectView.getSectionOfType(DirectorySection.KEY);
+        if (directorySection == null) {
+          continue;
+        }
+
+        for (DirectoryEntry entry : directorySection.items()) {
+          if (entry.included) {
+            continue;
+          }
+
+          WorkspacePath excludedDirectory = entry.directory;
+          if (FileUtil.isAncestor(
+            excludedDirectory.relativePath(),
+            includedDirectory.relativePath(),
+            false)) {
+            IssueOutput
+              .error(String.format("%s is included, but that contradicts %s which was excluded",
+                                   includedDirectory.toString(),
+                                   excludedDirectory.toString()))
+              .inFile(projectViewFile.projectViewFile)
+              .submit(context);
+            ok = false;
+          }
+        }
+      }
+    }
+    return ok;
+  }
+
+  private static List<WorkspacePath> getIncludedDirectories(ProjectViewSet projectViewSet) {
+    List<WorkspacePath> includedDirectories = Lists.newArrayList();
+    for (DirectoryEntry entry : projectViewSet.listItems(DirectorySection.KEY)) {
+      if (entry.included) {
+        includedDirectories.add(entry.directory);
+      }
+    }
+    return includedDirectories;
+  }
+
+  private static boolean verifyIncludedPackagesExistOnDisk(
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet) {
+    boolean ok = true;
+
+    WorkspaceScanner workspaceScanner = WorkspaceScanner.getInstance();
+
+    for (ProjectViewSet.ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
+      ListSection<DirectoryEntry> directorySection = projectViewFile.projectView.getSectionOfType(DirectorySection.KEY);
+      if (directorySection == null) {
+        continue;
+      }
+      for (DirectoryEntry entry : directorySection.items()) {
+        if (!entry.included) {
+          continue;
+        }
+        WorkspacePath workspacePath = entry.directory;
+        if (!workspaceScanner.exists(workspaceRoot, workspacePath)) {
+          IssueOutput
+            .error(String.format("Directory '%s' specified in import roots not found under workspace root '%s'",
+                                 workspacePath, workspaceRoot))
+            .inFile(projectViewFile.projectViewFile)
+            .withData(new MissingDirectoryIssueData(workspacePath))
+            .submit(context);
+          ok = false;
+        }
+      }
+    }
+    return ok;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java b/blaze-base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java
new file mode 100644
index 0000000..8d20d8b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/parser/ParseContext.java
@@ -0,0 +1,123 @@
+/*
+ * 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.projectview.parser;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+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.WorkspacePathResolver;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Context for the project view parser.
+ */
+public class ParseContext {
+  private final BlazeContext context;
+  private final WorkspacePathResolver workspacePathResolver;
+  @Nullable private final File file;
+  private final List<String> lines;
+
+  @Nullable private Line currentLine;
+  private int currentLineIndex;
+
+  public static class Line {
+    public final String text;
+    public final int indent;
+    public Line(String text, int indent) {
+      this.text = text;
+      this.indent = indent;
+    }
+  }
+
+  public ParseContext(BlazeContext context, WorkspacePathResolver workspacePathResolver, @Nullable File file, String text) {
+    this.context = context;
+    this.workspacePathResolver = workspacePathResolver;
+    this.file = file;
+    this.lines = Lists.newArrayList(text.split("\n"));
+    this.currentLine = null;
+    this.currentLineIndex = -1;
+    consume();
+  }
+
+  public Line current() {
+    assert currentLine != null;
+    return currentLine;
+  }
+
+  public void consume() {
+    while (++currentLineIndex < lines.size()) {
+      String line = lines.get(currentLineIndex);
+      int indent = 0;
+      while (indent < line.length() && line.charAt(indent) == ' ') {
+        ++indent;
+      }
+      if (!indentationCorrect(indent)) {
+        addError(String.format("Invalid indentation. Project view files are indented with %d spaces.", SectionParser.INDENT));
+        continue;
+      }
+
+      line = line.trim();
+      if (!shouldSkipLine(line)) {
+        currentLine = new Line(line, indent);
+        break;
+      }
+    }
+  }
+
+  private boolean indentationCorrect(int indent) {
+    return indent == 0 || indent == 2;
+  }
+
+  boolean shouldSkipLine(String line) {
+    return line.isEmpty() || line.startsWith("#");
+  }
+
+  public boolean atEnd() {
+    return currentLineIndex >= lines.size();
+  }
+
+  public BlazeContext getContext() {
+    return context;
+  }
+
+  public WorkspacePathResolver getWorkspacePathResolver() {
+    return workspacePathResolver;
+  }
+
+  @Nullable
+  public File getProjectViewFile() {
+    return file;
+  }
+
+  public void addErrors(List<BlazeValidationError> errors) {
+    for (BlazeValidationError error : errors) {
+      addError(error.getError());
+    }
+  }
+
+  public void addError(String error) {
+    IssueOutput
+      .error(error)
+      .inFile(file)
+      .onLine(currentLineIndex)
+      .submit(context);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java b/blaze-base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java
new file mode 100644
index 0000000..c9507a5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/parser/ProjectViewParser.java
@@ -0,0 +1,151 @@
+/*
+ * 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.projectview.parser;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+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.SectionParser;
+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.output.IssueOutput;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Parses and writes project views.
+ */
+public class ProjectViewParser {
+
+  private final BlazeContext context;
+  private final WorkspacePathResolver workspacePathResolver;
+  private final boolean recursive;
+
+  ImmutableList.Builder<ProjectViewSet.ProjectViewFile> projectViewFiles = ImmutableList.builder();
+
+  public ProjectViewParser(BlazeContext context,
+                           WorkspacePathResolver workspacePathResolver) {
+    this.context = context;
+    this.workspacePathResolver = workspacePathResolver;
+    this.recursive = true;
+  }
+
+  public void parseProjectView(File projectViewFile) {
+    String projectViewText = null;
+    try {
+      projectViewText = ProjectViewStorageManager.getInstance().loadProjectView(projectViewFile);
+    }
+    catch (IOException e) {
+      // Error handled below
+    }
+    if (projectViewText == null) {
+      IssueOutput.error(String.format("Could not load project view file: '%s'", projectViewFile.getPath()))
+        .submit(context);
+      return;
+    }
+    parseProjectView(new ParseContext(context, workspacePathResolver, projectViewFile, projectViewText));
+  }
+
+  public void parseProjectView(String text) {
+    parseProjectView(new ParseContext(context, workspacePathResolver, null, text));
+  }
+
+  private void parseProjectView(ParseContext parseContext) {
+    Map<SectionKey, Section> sectionMap = Maps.newHashMap();
+
+    while (!parseContext.atEnd()) {
+      if (parseContext.current().indent != 0) {
+        parseContext.addError(String.format("Invalid indentation on line: '%s'", parseContext.current().text));
+        skipSection(parseContext);
+        continue;
+      }
+      Section section = null;
+      SectionParser producingParser = null;
+      for (SectionParser sectionParser : Sections.getParsers()) {
+        section = sectionParser.parse(this, parseContext);
+        if (section != null) {
+          producingParser = sectionParser;
+          break;
+        }
+      }
+      if (section != null) {
+        SectionKey key = producingParser.getSectionKey();
+        if (!sectionMap.containsKey(key)) {
+          sectionMap.put(key, section);
+        } else {
+          BlazeContext context = parseContext.getContext();
+          IssueOutput.error(String.format("Duplicate attribute: '%s'", producingParser.getName()))
+            .inFile(parseContext.getProjectViewFile())
+            .submit(context);
+        }
+      } else {
+        parseContext.addError(String.format("Could not parse: '%s'", parseContext.current().text));
+        parseContext.consume();
+
+        // Skip past the entire section
+        skipSection(parseContext);
+      }
+    }
+
+    ProjectView.Builder builder = ProjectView.builder();
+    for (Map.Entry<SectionKey, Section> entry : sectionMap.entrySet()) {
+      builder.put(entry.getKey(), entry.getValue());
+    }
+
+    projectViewFiles.add(new ProjectViewSet.ProjectViewFile(builder.build(), parseContext.getProjectViewFile()));
+  }
+
+  /**
+   * Skips all lines until the next unindented, non-empty line.
+   */
+  private static void skipSection(ParseContext parseContext) {
+    while (!parseContext.atEnd() && parseContext.current().indent != 0) {
+      parseContext.consume();
+    }
+  }
+
+  public boolean isRecursive() {
+    return recursive;
+  }
+
+  public ProjectViewSet getResult() {
+    return new ProjectViewSet(projectViewFiles.build());
+  }
+
+  public static String projectViewToString(ProjectView projectView) {
+    StringBuilder sb = new StringBuilder();
+    int sectionCount = 0;
+    for (SectionParser sectionParser : Sections.getParsers()) {
+      SectionKey sectionKey = sectionParser.getSectionKey();
+      Section section = projectView.getSectionOfType(sectionKey);
+      if (section != null) {
+        if (sectionCount > 0) {
+          sb.append('\n');
+        }
+        sectionParser.print(sb, section);
+        ++sectionCount;
+      }
+    }
+    return sb.toString();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/Glob.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/Glob.java
new file mode 100644
index 0000000..29226cd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/Glob.java
@@ -0,0 +1,90 @@
+/*
+ * 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.projectview.section;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import com.intellij.openapi.fileTypes.FileNameMatcher;
+import org.jetbrains.jps.model.fileTypes.FileNameMatcherFactory;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * Glob matcher.
+ */
+public class Glob implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  private String pattern;
+  transient private FileNameMatcher matcher;
+
+  public Glob(String pattern) {
+    this.pattern = pattern;
+  }
+
+  public static class GlobSet implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private final Collection<Glob> globs = Lists.newArrayList();
+
+    public GlobSet(Collection<Glob> globs) {
+      this.globs.addAll(globs);
+    }
+
+    public boolean isEmpty() {
+      return globs.isEmpty();
+    }
+
+    public void add(Glob glob) {
+      globs.add(glob);
+    }
+
+    public boolean matches(String string) {
+      for (Glob glob : globs) {
+        if (glob.matches(string)) {
+          return true;
+        }
+      }
+      return false;
+    }
+  }
+
+  public boolean matches(String string) {
+    if (matcher == null) {
+      matcher = FileNameMatcherFactory.getInstance().createMatcher(pattern);
+    }
+    return matcher.accept(string);
+  }
+
+  @Override
+  public String toString() {
+    return pattern;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    Glob glob = (Glob)o;
+    return Objects.equal(pattern, glob.pattern);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(pattern);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/GlobSectionParser.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/GlobSectionParser.java
new file mode 100644
index 0000000..30e1a41
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/GlobSectionParser.java
@@ -0,0 +1,57 @@
+/*
+ * 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.projectview.section;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * Parses glob sections.
+ */
+public class GlobSectionParser extends ListSectionParser<Glob> {
+
+  public GlobSectionParser(SectionKey<Glob, ListSection<Glob>> key) {
+    super(key);
+  }
+
+  @Override
+  protected final void parseItem(ProjectViewParser parser,
+                                 ParseContext parseContext,
+                                 ImmutableList.Builder<Glob> items) {
+    String text = parseContext.current().text;
+    try {
+      Glob glob = new Glob(text);
+      items.add(glob);
+    }
+    catch (PatternSyntaxException e) {
+      parseContext.addError(e.getMessage());
+    }
+  }
+
+  @Override
+  protected final void printItem(Glob item, StringBuilder sb) {
+    sb.append(item.toString());
+  }
+
+  @Override
+  public ItemType getItemType() {
+    return ItemType.FileSystemItem;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/LabelSectionParser.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/LabelSectionParser.java
new file mode 100644
index 0000000..a34fdcc
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/LabelSectionParser.java
@@ -0,0 +1,58 @@
+/*
+ * 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.projectview.section;
+
+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.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Section of labels
+ */
+public final class LabelSectionParser extends ListSectionParser<Label> {
+  public LabelSectionParser(SectionKey<Label, ListSection<Label>> key) {
+    super(key);
+  }
+
+  @Override
+  protected void parseItem(@NotNull ProjectViewParser parser,
+                           @NotNull ParseContext parseContext,
+                           @NotNull ImmutableList.Builder<Label> items) {
+    String text = parseContext.current().text;
+    List<BlazeValidationError> errors = Lists.newArrayList();
+    if (!Label.validate(text, errors)) {
+      parseContext.addErrors(errors);
+      return;
+    }
+    items.add(new Label(text));
+  }
+
+  @Override
+  protected void printItem(@NotNull Label item, @NotNull StringBuilder sb) {
+    sb.append(item.toString());
+  }
+
+  @Override
+  public ItemType getItemType() {
+    return ItemType.Label;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/ListSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/ListSection.java
new file mode 100644
index 0000000..4bf8eba
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/ListSection.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section;
+
+import com.google.common.collect.ImmutableList;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+
+/**
+ * List value. Eg.
+ *
+ * my_attribute:
+ *  value0
+ *  value1
+ *  value2
+ *  ...
+ */
+public final class ListSection<T> extends Section<T> {
+  private static final long serialVersionUID = 1L;
+
+  private final ImmutableList<T> items;
+
+  ListSection(ImmutableList<T> items) {
+    this.items = items;
+  }
+
+  public Collection<T> items() {
+    return items;
+  }
+
+  public static <T> Builder<T> builder(SectionKey<T, ListSection<T>> sectionKey) {
+    return new Builder<T>(sectionKey, null);
+  }
+
+  public static <T> Builder<T> update(SectionKey<T, ListSection<T>> sectionKey, @Nullable ListSection<T> section) {
+    return new Builder<T>(sectionKey, section);
+  }
+
+  public static class Builder<T> extends SectionBuilder<T, ListSection<T>> {
+    private final ImmutableList.Builder<T> items = ImmutableList.builder();
+
+    public Builder(SectionKey<T, ListSection<T>> sectionKey, @Nullable ListSection<T> section) {
+      super(sectionKey);
+      if (section != null) {
+        items.addAll(section.items);
+      }
+    }
+
+    public final Builder<T> add(T item) {
+      items.add(item);
+      return this;
+    }
+
+    @Override
+    public final ListSection<T> build() {
+      return new ListSection<T>(items.build());
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/ListSectionParser.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/ListSectionParser.java
new file mode 100644
index 0000000..713bad2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/ListSectionParser.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.projectview.section;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+
+import javax.annotation.Nullable;
+
+/**
+ * List section parser base class.
+ */
+public abstract class ListSectionParser<T> extends SectionParser {
+  protected ListSectionParser(SectionKey<T, ? extends ListSection<T>> key) {
+    super(key);
+  }
+
+  @Nullable
+  @Override
+  public final Section parse(ProjectViewParser parser, ParseContext parseContext) {
+    if (parseContext.atEnd()) {
+      return null;
+    }
+
+    String name = getName();
+
+    if (!parseContext.current().text.equals(name + ':')) {
+      return null;
+    }
+    parseContext.consume();
+
+    ImmutableList.Builder<T> builder = ImmutableList.builder();
+
+    while (!parseContext.atEnd() && parseContext.current().indent >= SectionParser.INDENT) {
+      parseItem(parser, parseContext, builder);
+      parseContext.consume();
+    }
+
+    ImmutableList<T> items = builder.build();
+    if (items.isEmpty()) {
+      parseContext.addError(String.format("Empty section: '%s'", name));
+    }
+
+    return new ListSection<T>(items);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public final void print(StringBuilder sb, Section section) {
+    ListSection<T> listSection = (ListSection<T>)section;
+
+    // Omit empty sections completely
+    if (listSection.items().isEmpty()) {
+      return;
+    }
+
+    sb.append(getName()).append(':').append('\n');
+    for (T item : listSection.items()) {
+      for (int i = 0; i < SectionParser.INDENT; ++i) {
+        sb.append(' ');
+      }
+      printItem(item, sb);
+      sb.append('\n');
+    }
+  }
+
+  protected abstract void parseItem(ProjectViewParser parser,
+                                    ParseContext parseContext,
+                                    ImmutableList.Builder<T> items);
+
+  protected abstract void printItem(T item, StringBuilder sb);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/MetricsProjectSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/MetricsProjectSection.java
new file mode 100644
index 0000000..ea43916
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/MetricsProjectSection.java
@@ -0,0 +1,50 @@
+/*
+ * 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.projectview.section;
+
+import com.google.common.base.CharMatcher;
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+
+import javax.annotation.Nullable;
+
+public class MetricsProjectSection {
+  public static final SectionKey<String, ScalarSection<String>> KEY =
+    SectionKey.of("metrics_project");
+  public static final SectionParser PARSER = new MetricsProjectSectionParser();
+
+  private static class MetricsProjectSectionParser extends ScalarSectionParser<String> {
+    public MetricsProjectSectionParser() {
+      super(KEY, ':');
+    }
+
+    @Nullable
+    @Override
+    protected String parseItem(ProjectViewParser parser, ParseContext parseContext, String rest) {
+      return CharMatcher.is('\"').trimFrom(rest);
+    }
+
+    @Override
+    protected void printItem(StringBuilder sb, String value) {
+      sb.append(value);
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/ScalarSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/ScalarSection.java
new file mode 100644
index 0000000..d43ba63
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/ScalarSection.java
@@ -0,0 +1,55 @@
+/*
+ * 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.projectview.section;
+
+/**
+ * Scalar value.
+ */
+public final class ScalarSection<T> extends Section<T> {
+  private static final long serialVersionUID = 1L;
+
+  private final T value;
+
+  public ScalarSection(T value) {
+    this.value = value;
+  }
+
+  public T getValue() {
+    return value;
+  }
+
+  public static <T> Builder<T> builder(SectionKey<T, ScalarSection<T>> sectionKey) {
+    return new Builder<T>(sectionKey);
+  }
+
+  public static class Builder<T> extends SectionBuilder<T, ScalarSection<T>> {
+    private T value;
+
+    public Builder(SectionKey<T, ScalarSection<T>> sectionKey) {
+      super(sectionKey);
+    }
+
+    public Builder<T> set(T value) {
+      this.value = value;
+      return this;
+    }
+
+    @Override
+    public ScalarSection<T> build() {
+      return new ScalarSection<T>(value);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/ScalarSectionParser.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/ScalarSectionParser.java
new file mode 100644
index 0000000..7942372
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/ScalarSectionParser.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.base.projectview.section;
+
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+
+import javax.annotation.Nullable;
+
+/**
+ * Parses scalar values
+ */
+public abstract class ScalarSectionParser<T> extends SectionParser {
+
+  private final char divider;
+
+  protected ScalarSectionParser(SectionKey<T, ? extends ScalarSection<T>> sectionKey,
+                                char divider) {
+    super(sectionKey);
+    this.divider = divider;
+  }
+
+  @Nullable
+  @Override
+  public final ScalarSection<T> parse(ProjectViewParser parser, ParseContext parseContext) {
+    if (parseContext.atEnd()) {
+      return null;
+    }
+
+    String name = getName();
+    ParseContext.Line line = parseContext.current();
+
+    if (!line.text.startsWith(name + divider)) {
+      return null;
+    }
+    String rest = line.text.substring(name.length() + 1).trim();
+    parseContext.consume();
+    T item = parseItem(parser, parseContext, rest);
+    return item != null ? new ScalarSection<T>(item) : null;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public final void print(StringBuilder sb, Section section) {
+    sb.append(getName()).append(divider);
+    if (divider != ' ') {
+      sb.append(' ');
+    }
+    printItem(sb, ((ScalarSection<T>)section).getValue());
+    sb.append('\n');
+  }
+
+  /**
+   * Used by psi-parser for validation.
+   */
+  public char getDivider() {
+    return divider;
+  }
+
+  @Nullable
+  protected abstract T parseItem(ProjectViewParser parser, ParseContext parseContext, String rest);
+
+  protected abstract void printItem(StringBuilder sb, T value);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/Section.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/Section.java
new file mode 100644
index 0000000..6fc63d2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/Section.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section;
+
+import java.io.Serializable;
+
+/**
+ * A section is a part of an project view file. For instance:
+ *
+ * directories
+ *   java/com/a
+ *   java/com/b
+ *
+ * Is a directory section with two items.
+ */
+public abstract class Section<T> implements Serializable {
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionBuilder.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionBuilder.java
new file mode 100644
index 0000000..74ee9f5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionBuilder.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.projectview.section;
+
+/**
+ * Builder base class.
+ */
+public abstract class SectionBuilder<T, SectionType extends Section<T>> {
+  private final SectionKey<T, SectionType> sectionKey;
+
+  protected SectionBuilder(SectionKey<T, SectionType> sectionKey) {
+    this.sectionKey = sectionKey;
+  }
+
+  public abstract SectionType build();
+
+  public final SectionKey<T, SectionType> getSectionKey() {
+    return sectionKey;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionKey.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionKey.java
new file mode 100644
index 0000000..2c4084b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionKey.java
@@ -0,0 +1,53 @@
+/*
+ * 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.projectview.section;
+
+import com.google.common.base.Objects;
+
+import java.io.Serializable;
+
+/**
+ * Key to a section of type T.
+ */
+public final class SectionKey<T, SectionType extends Section<T>> implements Serializable {
+  private static final long serialVersionUID = 1L;
+  private final String name;
+
+  public SectionKey(String name) {
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public static <T, SectionType extends Section<T>> SectionKey<T, SectionType> of(String name) {
+    return new SectionKey<T, SectionType>(name);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    SectionKey<?, ?> that = (SectionKey<?, ?>)o;
+    return Objects.equal(name, that.name);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(name);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java
new file mode 100644
index 0000000..d01cad6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/SectionParser.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section;
+
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Parses a section.
+ */
+public abstract class SectionParser {
+
+  public static final int INDENT = 2;
+
+  /**
+   * The type of item(s) in this section
+   */
+  public enum ItemType {
+    FileSystemItem, // files, directories, globs
+    Label, // a blaze label
+    Other, // anything else
+  }
+
+  private final SectionKey sectionKey;
+
+  protected SectionParser(SectionKey sectionKey) {
+    this.sectionKey = sectionKey;
+  }
+
+  public String getName() {
+    return sectionKey.getName();
+  }
+
+  public SectionKey getSectionKey() {
+    return sectionKey;
+  }
+
+  @Nullable
+  public abstract Section parse(ProjectViewParser parser, ParseContext parseContext);
+
+  public abstract void print(StringBuilder sb, Section section);
+
+  public boolean isDeprecated() {
+    return false;
+  }
+
+  /**
+   * The type of item(s) in this section.
+   */
+  public abstract ItemType getItemType();
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java
new file mode 100644
index 0000000..692d385
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/AdditionalLanguagesSection.java
@@ -0,0 +1,63 @@
+/*
+ * 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.projectview.section.sections;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+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 org.jetbrains.annotations.NotNull;
+
+/**
+ * Allows users to set the rule classes they want to be imported
+ */
+public class AdditionalLanguagesSection {
+  public static final SectionKey<LanguageClass, ListSection<LanguageClass>> KEY = SectionKey.of("additional_languages");
+  public static final SectionParser PARSER = new AdditionalLanguagesSectionParser();
+
+  private static class AdditionalLanguagesSectionParser extends ListSectionParser<LanguageClass> {
+    public AdditionalLanguagesSectionParser() {
+      super(KEY);
+    }
+
+    @Override
+    protected void parseItem(@NotNull ProjectViewParser parser,
+                             @NotNull ParseContext parseContext,
+                             @NotNull ImmutableList.Builder<LanguageClass> items) {
+      String text = parseContext.current().text;
+      LanguageClass language = LanguageClass.fromString(text);
+      if (language == null) {
+        parseContext.addError("Invalid language: " + text);
+        return;
+      }
+      items.add(language);
+    }
+
+    @Override
+    protected void printItem(@NotNull LanguageClass item, @NotNull StringBuilder sb) {
+      sb.append(item.getName());
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java
new file mode 100644
index 0000000..d48cc0c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/BuildFlagsSection.java
@@ -0,0 +1,57 @@
+/*
+ * 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.projectview.section.sections;
+
+import com.google.common.collect.ImmutableList;
+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 org.jetbrains.annotations.NotNull;
+
+/**
+ * Section for blaze_flags
+ */
+public class BuildFlagsSection {
+  public static final SectionKey<String, ListSection<String>> KEY = SectionKey.of("build_flags");
+  public static final SectionParser PARSER = new BuildFlagsSectionParser();
+
+  public static class BuildFlagsSectionParser extends ListSectionParser<String> {
+    protected BuildFlagsSectionParser() {
+      super(KEY);
+    }
+
+    @Override
+    protected void parseItem(@NotNull ProjectViewParser parser,
+                             @NotNull ParseContext parseContext,
+                             @NotNull ImmutableList.Builder<String> items) {
+      String text = parseContext.current().text;
+      items.add(text);
+    }
+
+    @Override
+    protected void printItem(@NotNull String item, @NotNull StringBuilder sb) {
+      sb.append(item);
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/DirectoryEntry.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/DirectoryEntry.java
new file mode 100644
index 0000000..924c487
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/DirectoryEntry.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section.sections;
+
+import com.google.common.base.Objects;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+
+import java.io.Serializable;
+
+/**
+ * An entry in the directory section.
+ */
+public class DirectoryEntry implements Serializable {
+  private static final long serialVersionUID = 1L;
+  public final WorkspacePath directory;
+  public final boolean included;
+
+  public DirectoryEntry(WorkspacePath directory, boolean included) {
+    this.directory = directory;
+    this.included = included;
+  }
+
+  public static DirectoryEntry include(WorkspacePath directory) {
+    return new DirectoryEntry(directory, true);
+  }
+
+  public static DirectoryEntry exclude(WorkspacePath directory) {
+    return new DirectoryEntry(directory, false);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    DirectoryEntry that = (DirectoryEntry)o;
+    return Objects.equal(included, that.included)
+           && Objects.equal(directory, that.directory);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(directory, included);
+  }
+
+  @Override
+  public String toString() {
+    return (included ? "" : "-") + directoryString();
+  }
+
+  private String directoryString() {
+    if (directory.isWorkspaceRoot()) {
+      return ".";
+    }
+    return directory.relativePath();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java
new file mode 100644
index 0000000..b8e3329
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/DirectorySection.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section.sections;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+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 org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * "directories" section.
+ */
+public class DirectorySection {
+  public static final SectionKey<DirectoryEntry, ListSection<DirectoryEntry>> KEY = SectionKey.of("directories");
+  public static final SectionParser PARSER = new DirectorySectionParser();
+
+  private static class DirectorySectionParser extends ListSectionParser<DirectoryEntry> {
+    public DirectorySectionParser() {
+      super(KEY);
+    }
+
+    @Override
+    protected void parseItem(@NotNull ProjectViewParser parser,
+                             @NotNull ParseContext parseContext,
+                             @NotNull ImmutableList.Builder<DirectoryEntry> items) {
+      String text = parseContext.current().text;
+      boolean excluded = text.startsWith("-");
+      text = excluded ? text.substring(1) : text;
+
+      text = PathUtil.getCanonicalPath(text);
+
+      List<BlazeValidationError> errors = Lists.newArrayList();
+      if (WorkspacePath.validate(text, errors)) {
+        items.add(new DirectoryEntry(new WorkspacePath(text), !excluded));
+      } else {
+        parseContext.addErrors(errors);
+      }
+    }
+
+    @Override
+    protected void printItem(@NotNull DirectoryEntry item, @NotNull StringBuilder sb) {
+      sb.append(item.toString());
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.FileSystemItem;
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ExcludeTargetSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ExcludeTargetSection.java
new file mode 100644
index 0000000..8376747
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ExcludeTargetSection.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section.sections;
+
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.projectview.section.LabelSectionParser;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+
+/**
+ * Excludes a target.
+ */
+public class ExcludeTargetSection {
+  public static final SectionKey<Label, ListSection<Label>> KEY = SectionKey.of("exclude_target");
+  public static final SectionParser PARSER = new LabelSectionParser(KEY);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ExcludedSourceSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ExcludedSourceSection.java
new file mode 100644
index 0000000..da17d6b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ExcludedSourceSection.java
@@ -0,0 +1,32 @@
+/*
+ * 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.projectview.section.sections;
+
+import com.google.idea.blaze.base.projectview.section.*;
+
+/**
+ * Section for excluding source files.
+ */
+@Deprecated
+public class ExcludedSourceSection {
+  public static final SectionKey<Glob, ListSection<Glob>> KEY = SectionKey.of("excluded_sources");
+  public static final SectionParser PARSER = new GlobSectionParser(KEY) {
+    @Override
+    public boolean isDeprecated() {
+      return true;
+    }
+  };
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java
new file mode 100644
index 0000000..53a989d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ImportSection.java
@@ -0,0 +1,75 @@
+/*
+ * 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.projectview.section.sections;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+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.ui.BlazeValidationError;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * "import" section.
+ */
+public class ImportSection {
+  public static final SectionKey<WorkspacePath, ScalarSection<WorkspacePath>> KEY = SectionKey.of("import");
+  public static final SectionParser PARSER = new ImportSectionParser();
+
+  private static class ImportSectionParser extends ScalarSectionParser<WorkspacePath> {
+    public ImportSectionParser() {
+      super(KEY, ' ');
+    }
+
+    @Override
+    @Nullable
+    protected WorkspacePath parseItem(@NotNull ProjectViewParser parser, @NotNull ParseContext parseContext, @NotNull String text) {
+      List<BlazeValidationError> errors = Lists.newArrayList();
+      if (WorkspacePath.validate(text, errors)) {
+        WorkspacePath workspacePath = new WorkspacePath(text);
+        if (parser.isRecursive()) {
+          File projectViewFile = parseContext.getWorkspacePathResolver().resolveToFile(workspacePath);
+          if (projectViewFile != null) {
+            parser.parseProjectView(projectViewFile);
+          } else {
+            parseContext.addError("Could not resolve import: " + workspacePath);
+          }
+        }
+        return workspacePath;
+      }
+      parseContext.addErrors(errors);
+      return null;
+    }
+
+    @Override
+    protected void printItem(@NotNull StringBuilder sb, @NotNull WorkspacePath section) {
+      sb.append(section.toString());
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.FileSystemItem;
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ImportTargetOutputSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ImportTargetOutputSection.java
new file mode 100644
index 0000000..13824e7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/ImportTargetOutputSection.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section.sections;
+
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.projectview.section.LabelSectionParser;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.SectionKey;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+
+/**
+ * Forces target output import of mentioned targets.
+ */
+public class ImportTargetOutputSection {
+  public static final SectionKey<Label, ListSection<Label>> KEY = SectionKey.of("import_target_output");
+  public static final SectionParser PARSER = new LabelSectionParser(KEY);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java
new file mode 100644
index 0000000..9d3784e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/Sections.java
@@ -0,0 +1,59 @@
+/*
+ * 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.projectview.section.sections;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.projectview.section.MetricsProjectSection;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * List of available sections.
+ */
+public class Sections {
+  // Put these in the order that you want them to appear in the output
+  private static final List<SectionParser> PARSERS = Lists.newArrayList(
+    ImportSection.PARSER,
+    DirectorySection.PARSER,
+    TargetSection.PARSER,
+    WorkspaceTypeSection.PARSER,
+    AdditionalLanguagesSection.PARSER,
+    TestSourceSection.PARSER,
+    BuildFlagsSection.PARSER,
+    ImportTargetOutputSection.PARSER,
+    ExcludeTargetSection.PARSER,
+    ExcludedSourceSection.PARSER,
+    MetricsProjectSection.PARSER
+  );
+
+  public static List<SectionParser> getParsers() {
+    List<SectionParser> parsers = Lists.newArrayList(PARSERS);
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      parsers.addAll(syncPlugin.getSections());
+    }
+    return parsers;
+  }
+
+  public static List<SectionParser> getUndeprecatedParsers() {
+    return getParsers().stream()
+      .filter(p -> !p.isDeprecated())
+      .collect(Collectors.toList());
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java
new file mode 100644
index 0000000..8a737fb
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/TargetSection.java
@@ -0,0 +1,58 @@
+/*
+ * 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.projectview.section.sections;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+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 org.jetbrains.annotations.NotNull;
+
+/**
+ * "targets" section.
+ */
+public class TargetSection {
+  public static final SectionKey<TargetExpression, ListSection<TargetExpression>> KEY = SectionKey.of("targets");
+  public static final SectionParser PARSER = new TargetSectionParser();
+
+  private static class TargetSectionParser extends ListSectionParser<TargetExpression> {
+    public TargetSectionParser() {
+      super(KEY);
+    }
+
+    @Override
+    protected void parseItem(@NotNull ProjectViewParser parser,
+                             @NotNull ParseContext parseContext,
+                             @NotNull ImmutableList.Builder<TargetExpression> items) {
+      String text = parseContext.current().text;
+      items.add(TargetExpression.fromString(text));
+    }
+
+    @Override
+    protected void printItem(@NotNull TargetExpression item, @NotNull StringBuilder sb) {
+      sb.append(item.toString());
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Label;
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/TestSourceSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/TestSourceSection.java
new file mode 100644
index 0000000..4cbc6bc
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/TestSourceSection.java
@@ -0,0 +1,26 @@
+/*
+ * 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.projectview.section.sections;
+
+import com.google.idea.blaze.base.projectview.section.*;
+
+/**
+ * Section for configuring test sources.
+ */
+public class TestSourceSection {
+  public static final SectionKey<Glob, ListSection<Glob>> KEY = SectionKey.of("test_sources");
+  public static final SectionParser PARSER = new GlobSectionParser(KEY);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/WorkspaceTypeSection.java b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/WorkspaceTypeSection.java
new file mode 100644
index 0000000..abab2ed
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/projectview/section/sections/WorkspaceTypeSection.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.projectview.section.sections;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+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.ui.BlazeValidationError;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * The type of your workspace.
+ */
+public class WorkspaceTypeSection {
+  public static final SectionKey<WorkspaceType, ScalarSection<WorkspaceType>> KEY = SectionKey.of("workspace_type");
+  public static final SectionParser PARSER = new WorkspaceTypeSectionParser();
+
+  private static class WorkspaceTypeSectionParser extends ScalarSectionParser<WorkspaceType> {
+    public WorkspaceTypeSectionParser() {
+      super(KEY, ':');
+    }
+
+    @Override
+    @Nullable
+    protected WorkspaceType parseItem(ProjectViewParser parser, ParseContext parseContext, String text) {
+      List<BlazeValidationError> errors = Lists.newArrayList();
+      WorkspaceType workspaceType = WorkspaceType.fromString(text);
+      if (workspaceType == null) {
+        parseContext.addError("Invalid workspace type: " + text);
+      }
+      parseContext.addErrors(errors);
+      return workspaceType;
+    }
+
+    @Override
+    protected void printItem(StringBuilder sb, WorkspaceType item) {
+      sb.append(item.toString());
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/rulemaps/ReverseDependencyMap.java b/blaze-base/src/com/google/idea/blaze/base/rulemaps/ReverseDependencyMap.java
new file mode 100644
index 0000000..84e073b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/rulemaps/ReverseDependencyMap.java
@@ -0,0 +1,39 @@
+/*
+ * 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.rulemaps;
+
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+
+import java.util.Map;
+
+public class ReverseDependencyMap {
+  public static ImmutableMultimap<Label, Label> createRdepsMap(Map<Label, RuleIdeInfo> ruleMap) {
+    ImmutableMultimap.Builder<Label, Label> builder = ImmutableMultimap.builder();
+    for (Map.Entry<Label, RuleIdeInfo> entry : ruleMap.entrySet()) {
+      Label label = entry.getKey();
+      RuleIdeInfo ruleIdeInfo = entry.getValue();
+      for (Label dep : Iterables.concat(ruleIdeInfo.dependencies, ruleIdeInfo.runtimeDeps)) {
+        if (ruleMap.containsKey(dep)) {
+          builder.put(dep, label);
+        }
+      }
+    }
+    return builder.build();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMap.java b/blaze-base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMap.java
new file mode 100644
index 0000000..2b10c82
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMap.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.rulemaps;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import java.io.File;
+
+/**
+ * Maps source files to their respective targets
+ */
+public interface SourceToRuleMap {
+
+  static SourceToRuleMap getInstance(Project project) {
+    return ServiceManager.getService(project, SourceToRuleMap.class);
+  }
+
+  ImmutableCollection<Label> getTargetsForSourceFile(File file);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMapImpl.java b/blaze-base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMapImpl.java
new file mode 100644
index 0000000..c330077
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/rulemaps/SourceToRuleMapImpl.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.rulemaps;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * Maps source files to their respective targets
+ */
+public class SourceToRuleMapImpl implements SourceToRuleMap {
+  private final Project project;
+  private ImmutableMultimap<File, Label> sourceToTargetMap;
+
+  public static SourceToRuleMapImpl getImpl(Project project) {
+    return (SourceToRuleMapImpl) ServiceManager.getService(project, SourceToRuleMap.class);
+  }
+
+  public SourceToRuleMapImpl(Project project) {
+    this.project = project;
+  }
+
+  @Override
+  public ImmutableCollection<Label> getTargetsForSourceFile(File file) {
+    ImmutableMultimap<File, Label> sourceToTargetMap = getSourceToTargetMap();
+    return sourceToTargetMap != null ? sourceToTargetMap.get(file) : ImmutableList.of();
+  }
+
+  @Nullable
+  private synchronized ImmutableMultimap<File, Label> getSourceToTargetMap() {
+    if (this.sourceToTargetMap == null) {
+      this.sourceToTargetMap = initSourceToTargetMap();
+    }
+    return this.sourceToTargetMap;
+  }
+
+  private synchronized void clearSourceToTargetMap() {
+    this.sourceToTargetMap = null;
+  }
+
+  @Nullable
+  private ImmutableMultimap<File, Label> initSourceToTargetMap() {
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
+    ImmutableMultimap.Builder<File, Label> sourceToTargetMap = ImmutableMultimap.builder();
+    for (RuleIdeInfo rule : blazeProjectData.ruleMap.values()) {
+      Label label = rule.label;
+      for (ArtifactLocation sourceArtifact : rule.sources) {
+        sourceToTargetMap.put(sourceArtifact.getFile(), label);
+      }
+    }
+    return sourceToTargetMap.build();
+  }
+
+  static class ClearSourceToTargetMap extends SyncListener.Adapter {
+    @Override
+    public void onSyncComplete(Project project,
+                               BlazeImportSettings importSettings,
+                               ProjectViewSet projectViewSet,
+                               BlazeProjectData blazeProjectData) {
+      getImpl(project).clearSourceToTargetMap();
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/BlazeRuleConfigurationFactory.java b/blaze-base/src/com/google/idea/blaze/base/run/BlazeRuleConfigurationFactory.java
new file mode 100644
index 0000000..87ab7c7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/BlazeRuleConfigurationFactory.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.openapi.extensions.ExtensionPointName;
+
+/**
+ * A factory creating run configurations based on Blaze rules.
+ */
+public interface BlazeRuleConfigurationFactory {
+  ExtensionPointName<BlazeRuleConfigurationFactory> EP_NAME =
+    ExtensionPointName.create("com.google.idea.blaze.RuleConfigurationFactory");
+
+  /**
+   * Returns whether this factory can handle a rule.
+   */
+  boolean handlesRule(WorkspaceLanguageSettings workspaceLanguageSettings, RuleIdeInfo rule);
+
+  /** Constructs and initializes a configuration for the given rule. */
+  RunnerAndConfigurationSettings createForRule(RunManager runManager, RuleIdeInfo rule);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java b/blaze-base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
new file mode 100644
index 0000000..f9b6865
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/BlazeRunConfiguration.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Marker interface for all run configurations
+ */
+public interface BlazeRunConfiguration {
+  @Nullable
+  TargetExpression getTarget();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java b/blaze-base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
new file mode 100755
index 0000000..4f81112
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/BlazeRunConfigurationSyncListener.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+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.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.ui.UIUtil;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Creates run configurations for modules imported from
+ * {@link com.android.builder.model.AndroidProject}s.
+ */
+public class BlazeRunConfigurationSyncListener implements SyncListener {
+  private static Logger log = Logger.getInstance(BlazeRunConfigurationSyncListener.class);
+
+  @Override
+  public void onSyncStart(Project project) {
+  }
+
+  @Override
+  public void afterSync(Project project,
+                        boolean successful) {
+  }
+
+  @Override
+  public void onSyncComplete(
+    Project project,
+    BlazeImportSettings importSettings,
+    ProjectViewSet projectViewSet,
+    BlazeProjectData blazeProjectData) {
+
+    UIUtil.invokeAndWaitIfNeeded((Runnable)() -> {
+      Set<Label> labelsWithConfigs = labelsWithConfigs(project);
+      Set<TargetExpression> targetExpressions = Sets.newHashSet(projectViewSet.listItems(TargetSection.KEY));
+      for (RuleIdeInfo rule : blazeProjectData.ruleMap.values()) {
+        maybeAddRunConfiguration(project, blazeProjectData.workspaceLanguageSettings, targetExpressions, labelsWithConfigs, rule);
+      }
+    });
+  }
+
+  /** Collects a set of all the Blaze labels that have an associated run configuration. */
+  private static Set<Label> labelsWithConfigs(Project project) {
+    List<RunConfiguration> configurations =
+      RunManager.getInstance(project).getAllConfigurationsList();
+    Set<Label> labelsWithConfigs = Sets.newHashSet();
+    for (RunConfiguration configuration : configurations) {
+      if (configuration instanceof BlazeRunConfiguration) {
+        BlazeRunConfiguration blazeRunConfiguration =
+          (BlazeRunConfiguration) configuration;
+        TargetExpression target = blazeRunConfiguration.getTarget();
+        if (target instanceof Label) {
+          labelsWithConfigs.add((Label)target);
+        }
+      }
+    }
+    return labelsWithConfigs;
+  }
+
+  /**
+   * Adds a run configuration for an android_binary target if there is not already a configuration
+   * for that target.
+   */
+  private static void maybeAddRunConfiguration(
+    Project project,
+    WorkspaceLanguageSettings workspaceLanguageSettings,
+    Set<TargetExpression> importTargets,
+    Set<Label> labelsWithConfigs,
+    RuleIdeInfo rule) {
+    Label label = rule.label;
+    // We only auto-generate configurations for rules listed in the project view.
+    if (!importTargets.contains(label) ||
+        labelsWithConfigs.contains(label)) {
+      return;
+    }
+    labelsWithConfigs.add(label);
+    final RunManager runManager = RunManager.getInstance(project);
+
+    for (BlazeRuleConfigurationFactory configurationFactory : BlazeRuleConfigurationFactory.EP_NAME.getExtensions()) {
+      if (configurationFactory.handlesRule(workspaceLanguageSettings, rule)) {
+        final RunnerAndConfigurationSettings settings = configurationFactory.createForRule(runManager, rule);
+        runManager.addConfiguration(settings, false /* isShared */);
+        if (runManager.getSelectedConfiguration() == null) {
+          // TODO(joshgiles): Better strategy for picking initially selected config.
+          runManager.setSelectedConfiguration(settings);
+        }
+        break;
+      }
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/TestRuleFinder.java b/blaze-base/src/com/google/idea/blaze/base/run/TestRuleFinder.java
new file mode 100644
index 0000000..28e7da2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/TestRuleFinder.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run;
+
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Collection;
+
+/**
+ * Locates test rules for a given file.
+ */
+public interface TestRuleFinder {
+  static TestRuleFinder getInstance(Project project) {
+    return ServiceManager.getService(project, TestRuleFinder.class);
+  }
+
+  Collection<Label> testTargetsForSourceFile(File sourceFile, @Nullable TestIdeInfo.TestSize testSize);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/processhandler/LineProcessingProcessAdapter.java b/blaze-base/src/com/google/idea/blaze/base/run/processhandler/LineProcessingProcessAdapter.java
new file mode 100644
index 0000000..261d38b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/processhandler/LineProcessingProcessAdapter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.processhandler;
+
+import com.google.common.base.Charsets;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.intellij.execution.process.ProcessAdapter;
+import com.intellij.execution.process.ProcessEvent;
+import com.intellij.openapi.util.Key;
+
+import java.io.IOException;
+
+public final class LineProcessingProcessAdapter extends ProcessAdapter {
+  private final LineProcessingOutputStream myOutputStream;
+
+  public LineProcessingProcessAdapter(LineProcessingOutputStream outputStream) {
+    myOutputStream = outputStream;
+  }
+
+  @Override
+  public void onTextAvailable(ProcessEvent event, Key outputType) {
+    String text = event.getText();
+    if (text != null) {
+      try {
+        myOutputStream.write(text.getBytes(Charsets.UTF_8));
+      }
+      catch (IOException e) {
+        // Ignore -- cannot happen
+      }
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/processhandler/ScopedBlazeProcessHandler.java b/blaze-base/src/com/google/idea/blaze/base/run/processhandler/ScopedBlazeProcessHandler.java
new file mode 100644
index 0000000..abf486e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/processhandler/ScopedBlazeProcessHandler.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.processhandler;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.*;
+import com.intellij.openapi.util.Key;
+
+/**
+ * Scoped process handler.
+ *
+ * A context is created during construction and is ended when the process is terminated.
+ */
+public final class ScopedBlazeProcessHandler extends KillableColoredProcessHandler {
+  /**
+   * Methods to give the caller of {@link ScopedBlazeProcessHandler} hooks after the context is created.
+   */
+  public interface ScopedProcessHandlerDelegate {
+    /**
+     * This method is called when the process starts. Any context setup (like pushing scopes on the context) should be done here.
+     */
+    void onBlazeContextStart(BlazeContext context);
+
+    /**
+     * Get a list of process listeners to add to the process.
+     */
+    ImmutableList<ProcessListener> createProcessListeners(BlazeContext context);
+
+  }
+
+  private final ScopedProcessHandlerDelegate scopedProcessHandlerDelegate;
+  private final BlazeContext context;
+
+  /**
+   * Construct a process handler and a context to be used for the life of the process.
+   *
+   * @param blazeCommand the blaze command to run
+   * @param workspaceRoot workspace root
+   * @param scopedProcessHandlerDelegate delegate methods that will be run with the process's context.
+   * @throws ExecutionException
+   */
+  public ScopedBlazeProcessHandler(
+    BlazeCommand blazeCommand,
+    WorkspaceRoot workspaceRoot,
+    ScopedProcessHandlerDelegate scopedProcessHandlerDelegate) throws ExecutionException {
+    super(new GeneralCommandLine(blazeCommand.toList()).withWorkDirectory(workspaceRoot.directory().getPath()));
+
+    this.scopedProcessHandlerDelegate = scopedProcessHandlerDelegate;
+    this.context = new BlazeContext();
+    // The context is released in the ScopedProcessHandlerListener.
+    this.context.hold();
+
+    for (ProcessListener processListener : scopedProcessHandlerDelegate.createProcessListeners(context)) {
+      addProcessListener(processListener);
+    }
+    addProcessListener(new ScopedProcessHandlerListener());
+  }
+
+  @Override
+  public void coloredTextAvailable(String text, Key attributes) {
+    // Change blaze's stderr output to normal color, otherwise
+    // test output looks red
+    if (attributes == ProcessOutputTypes.STDERR) {
+      attributes = ProcessOutputTypes.STDOUT;
+    }
+
+    super.coloredTextAvailable(text, attributes);
+  }
+
+  /**
+   * Handle the {@link BlazeContext} held in a {@link ScopedBlazeProcessHandler}. This class will take care of calling methods when the
+   * process starts and freeing the context when the process terminates.
+   */
+  private class ScopedProcessHandlerListener extends ProcessAdapter {
+
+    @Override
+    public void startNotified(ProcessEvent event) {
+      scopedProcessHandlerDelegate.onBlazeContextStart(context);
+    }
+
+    @Override
+    public void processWillTerminate(ProcessEvent event, boolean willBeDestroyed) {
+      context.release();
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinder.java b/blaze-base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinder.java
new file mode 100644
index 0000000..89441f2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinder.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.base.run.rulefinder;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Searches BlazeProjectData for matching rules.
+ */
+public abstract class RuleFinder {
+  public static RuleFinder getInstance() {
+    return ServiceManager.getService(RuleFinder.class);
+  }
+
+  @Nullable
+  public RuleIdeInfo ruleForTarget(Project project, final Label target) {
+    return findRule(project, input -> input.label.equals(target));
+  }
+
+  public ImmutableList<RuleIdeInfo> rulesOfKinds(
+    Project project, final Kind... kinds) {
+    return rulesOfKinds(project, Arrays.asList(kinds));
+  }
+
+  public ImmutableList<RuleIdeInfo> rulesOfKinds(
+    Project project, final List<Kind> kinds) {
+    return ImmutableList.copyOf(findRules(project, input -> input.kindIsOneOf(kinds)));
+  }
+
+  @Nullable
+  public RuleIdeInfo firstRuleOfKinds(Project project, Kind... kinds) {
+    return Iterables.getFirst(rulesOfKinds(project, kinds), null);
+  }
+
+  @Nullable
+  public RuleIdeInfo firstRuleOfKinds(Project project, List<Kind> kinds) {
+    return Iterables.getFirst(rulesOfKinds(project, kinds), null);
+  }
+
+  @Nullable
+  private RuleIdeInfo findRule(Project project, Predicate<RuleIdeInfo> predicate) {
+    List<RuleIdeInfo> results = findRules(project, predicate);
+    assert results.size() <= 1;
+    return Iterables.getFirst(results, null);
+  }
+
+  @Nullable
+  public RuleIdeInfo findFirstRule(Project project, Predicate<RuleIdeInfo> predicate) {
+    return Iterables.getFirst(findRules(project, predicate), null);
+  }
+
+  public abstract List<RuleIdeInfo> findRules(Project project, Predicate<RuleIdeInfo> predicate);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinderImpl.java b/blaze-base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinderImpl.java
new file mode 100644
index 0000000..b07943c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/rulefinder/RuleFinderImpl.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.base.run.rulefinder;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Implementation of RuleFinder.
+ */
+class RuleFinderImpl extends RuleFinder {
+  @Override
+  public List<RuleIdeInfo> findRules(@NotNull Project project, @NotNull Predicate<RuleIdeInfo> predicate) {
+    BlazeProjectData projectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return ImmutableList.of();
+    }
+
+    ImmutableList.Builder<RuleIdeInfo> resultList = ImmutableList.builder();
+    for (RuleIdeInfo rule : projectData.ruleMap.values()) {
+      if (predicate.apply(rule)) {
+        resultList.add(rule);
+      }
+    }
+    return resultList.build();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/run/testmap/TestRuleFinderImpl.java b/blaze-base/src/com/google/idea/blaze/base/run/testmap/TestRuleFinderImpl.java
new file mode 100644
index 0000000..91e8f6d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/run/testmap/TestRuleFinderImpl.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.testmap;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.*;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.TestRuleFinder;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Used to locate tests from source files for things like right-clicks.
+ *
+ * It's essentially a map from source file -> reachable test rules.
+ */
+public class TestRuleFinderImpl implements TestRuleFinder {
+
+  // Safety experiment to allow us to turn this off. Deployed in ijwb 1.2.
+  private static final BoolExperiment USE_TEST_SIZE = new BoolExperiment("use.test.sizes", true);
+
+  private final Project project;
+  @Nullable
+  private TestMap testMap;
+
+  static class TestMap {
+    private final Project project;
+    private final Multimap<File, Label> rootsMap;
+    private final ImmutableMap<Label, RuleIdeInfo> ruleMap;
+
+    TestMap(Project project, ImmutableMap<Label, RuleIdeInfo> ruleMap) {
+      this.project = project;
+      this.rootsMap = createRootsMap(ruleMap.values());
+      this.ruleMap = ruleMap;
+    }
+
+    public Collection<Label> testTargetsForSourceFile(File sourceFile,
+                                                      @Nullable TestIdeInfo.TestSize testSize) {
+      BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+      if (blazeProjectData != null) {
+        if (!USE_TEST_SIZE.getValue()) {
+          testSize = null;
+        }
+        // If testSize == null then do a pass preferring small
+        // Some test runners will assume no size annotation == small and filter on that, others will not
+        else if (testSize == null) {
+          Collection<Label> smallResults = testTargetsForSourceFile(
+            blazeProjectData.reverseDependencies,
+            sourceFile,
+            TestIdeInfo.DEFAULT_NON_ANNOTATED_TEST_SIZE);
+
+          if (!smallResults.isEmpty()) {
+            return smallResults;
+          }
+        }
+
+        return testTargetsForSourceFile(blazeProjectData.reverseDependencies, sourceFile, testSize);
+      }
+      return ImmutableList.of();
+    }
+
+    @VisibleForTesting
+    Collection<Label> testTargetsForSourceFile(ImmutableMultimap<Label, Label> rdepsMap,
+                                               File sourceFile,
+                                               @Nullable TestIdeInfo.TestSize testSize) {
+      List<Label> result = Lists.newArrayList();
+      Collection<Label> roots = rootsMap.get(sourceFile);
+
+      Queue<Label> todo = Queues.newArrayDeque();
+      for (Label label : roots) {
+        todo.add(label);
+      }
+      Set<Label> seen = Sets.newHashSet();
+      while (!todo.isEmpty()) {
+        Label label = todo.remove();
+        if (!seen.add(label)) {
+          continue;
+        }
+
+        RuleIdeInfo rule = ruleMap.get(label);
+        if (isTestRule(rule) && matchesTestSize(rule, testSize)) {
+          result.add(label);
+        }
+        for (Label rdep : rdepsMap.get(label)) {
+          todo.add(rdep);
+        }
+      }
+      return result;
+    }
+
+    static Multimap<File, Label> createRootsMap(Collection<RuleIdeInfo> rules) {
+      Multimap<File, Label> result = ArrayListMultimap.create();
+      for (RuleIdeInfo ruleIdeInfo : rules) {
+        for (ArtifactLocation source : ruleIdeInfo.sources) {
+          result.put(source.getFile(), ruleIdeInfo.label);
+        }
+      }
+      return result;
+    }
+
+    private static boolean isTestRule(@Nullable RuleIdeInfo rule) {
+      return rule != null && rule.kind != null && rule.kind.isOneOf(
+        Kind.ANDROID_ROBOLECTRIC_TEST,
+        Kind.ANDROID_TEST,
+        Kind.JAVA_TEST,
+        Kind.GWT_TEST
+      );
+    }
+  }
+
+  private static boolean matchesTestSize(RuleIdeInfo rule, @Nullable TestIdeInfo.TestSize testSize) {
+    if (testSize == null) {
+      return true;
+    }
+    TestIdeInfo.TestSize ruleTestSize = TestIdeInfo.getTestSize(rule);
+    if (ruleTestSize == null) {
+      return true;
+    }
+    return ruleTestSize == testSize;
+  }
+
+  public TestRuleFinderImpl(Project project) {
+    this.project = project;
+  }
+
+  @Override
+  public Collection<Label> testTargetsForSourceFile(File sourceFile, @Nullable TestIdeInfo.TestSize testSize) {
+    TestMap testMap = getTestMap();
+    if (testMap == null) {
+      return ImmutableList.of();
+    }
+    return testMap.testTargetsForSourceFile(sourceFile, testSize);
+  }
+
+  private synchronized TestMap getTestMap() {
+    if (testMap == null) {
+      testMap = initTestMap();
+    }
+    return testMap;
+  }
+
+  @Nullable
+  private TestMap initTestMap() {
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return null;
+    }
+    return new TestMap(project, blazeProjectData.ruleMap);
+  }
+
+  private synchronized void clearMapData() {
+    this.testMap = null;
+  }
+
+  static class ClearTestMap extends SyncListener.Adapter {
+    @Override
+    public void onSyncComplete(Project project,
+                               BlazeImportSettings importSettings,
+                               ProjectViewSet projectViewSet,
+                               BlazeProjectData blazeProjectData) {
+      TestRuleFinder testRuleFinder = TestRuleFinder.getInstance(project);
+      ((TestRuleFinderImpl) testRuleFinder).clearMapData();
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/BlazeContext.java b/blaze-base/src/com/google/idea/blaze/base/scope/BlazeContext.java
new file mode 100644
index 0000000..83f1d0f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/BlazeContext.java
@@ -0,0 +1,268 @@
+/*
+ * 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.scope;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Scoped operation context.
+ */
+public class BlazeContext {
+  @Nullable
+  private BlazeContext parentContext;
+
+  @NotNull
+  private final List<BlazeScope> scopes = Lists.newArrayList();
+
+  @NotNull
+  private final ArrayListMultimap<Class<? extends Output>, OutputSink<?>> outputSinks = ArrayListMultimap.create();
+
+  boolean isEnding;
+
+  boolean isCancelled;
+
+  private int holdCount;
+
+  private boolean hasErrors;
+
+  private boolean propagatesErrors = true;
+
+  public BlazeContext() {
+    this(null);
+  }
+
+  public BlazeContext(@Nullable BlazeContext parentContext) {
+    this.parentContext = parentContext;
+  }
+
+  public BlazeContext push(@NotNull BlazeScope scope) {
+    scopes.add(scope);
+    scope.onScopeBegin(this);
+    return this;
+  }
+
+  /**
+   * Ends the context scope.
+   */
+  public void endScope() {
+    if (isEnding || holdCount > 0) {
+      return;
+    }
+    isEnding = true;
+    for (int i = scopes.size() - 1; i >= 0; i--) {
+      scopes.get(i).onScopeEnd(this);
+    }
+
+    if (parentContext != null && hasErrors && propagatesErrors) {
+      parentContext.setHasError();
+    }
+  }
+
+  /**
+   * Requests cancellation of the operation.
+   * <p/>
+   * <p>Each context holder must handle cancellation individually.
+   */
+  public void setCancelled() {
+    if (isEnding || isCancelled) {
+      return;
+    }
+
+    isCancelled = true;
+
+    if (parentContext != null) {
+      parentContext.setCancelled();
+    }
+  }
+
+  public void hold() {
+    ++holdCount;
+  }
+
+  public void release() {
+    if (--holdCount == 0) {
+      endScope();
+    }
+  }
+
+  public boolean isEnding() {
+    return isEnding;
+  }
+
+  public boolean isCancelled() {
+    return isCancelled;
+  }
+
+  @Nullable
+  public <T extends BlazeScope> T getScope(@NotNull Class<T> scopeClass) {
+    return getScope(scopeClass, scopes.size());
+  }
+
+  @Nullable
+  private <T extends BlazeScope> T getScope(@NotNull Class<T> scopeClass, int endIndex) {
+    for (int i = endIndex - 1; i >= 0; i--) {
+      if (scopes.get(i).getClass() == scopeClass) {
+        return scopeClass.cast(scopes.get(i));
+      }
+    }
+    if (parentContext != null) {
+      return parentContext.getScope(scopeClass);
+    }
+    return null;
+  }
+
+  @Nullable
+  public <T extends BlazeScope> T getParentScope(@NotNull T scope) {
+    int index = scopes.indexOf(scope);
+    if (index == -1) {
+      throw new IllegalArgumentException("Scope does not belong to this context.");
+    }
+    @SuppressWarnings("unchecked")
+    Class<T> scopeClass = (Class<T>)scope.getClass();
+    return getScope(scopeClass, index);
+  }
+
+  /**
+   * Find all instances of {@param scopeClass} that are on the stack starting with this context.
+   * That includes this context and all parent contexts recursively.
+   *
+   * @param scopeClass type of scopes to locate
+   * @return The ordered list of all scopes of type {@param scopeClass}, ordered from
+   * {@param startingScope} to the root.
+   */
+  @NotNull
+  public <T extends BlazeScope> List<T> getScopes(@NotNull Class<T> scopeClass) {
+    List<T> scopesCollector = Lists.newArrayList();
+    getScopes(scopesCollector, scopeClass, scopes.size());
+    return scopesCollector;
+  }
+
+  /**
+   * Find all instances of {@param scopeClass} that are above {@param startingScope} on the stack.
+   * That includes this context and all parent contexts recursively. {@param startingScope} must be
+   * in the this {@link BlazeContext}.
+   *
+   * @param scopeClass    type of scopes to locate
+   * @param startingScope scope to start our search from
+   * @return If {@param startingScope} is in this context, the ordered list of all scopes of type
+   * {@param scopeClass}, ordered from {@param startingScope} to the root. Otherwise, an empty
+   * list.
+   */
+  @NotNull
+  public <T extends BlazeScope> List<T> getScopes(
+    @NotNull Class<T> scopeClass,
+    @NotNull BlazeScope startingScope) {
+    List<T> scopesCollector = Lists.newArrayList();
+    int index = scopes.indexOf(startingScope);
+    if (index == -1) {
+      return scopesCollector;
+    }
+
+    // index + 1 so we include startingScope
+    getScopes(scopesCollector, scopeClass, index + 1);
+    return scopesCollector;
+  }
+
+  /**
+   * Add matching scopes to {@param scopesCollector}. Search from {@param maxIndex} - 1 to 0.
+   */
+  @VisibleForTesting
+  <T extends BlazeScope> void getScopes(
+    @NotNull List<T> scopesCollector,
+    @NotNull Class<T> scopeClass,
+    int maxIndex) {
+    for (int i = maxIndex - 1; i >= 0; --i) {
+      BlazeScope scope = scopes.get(i);
+      if (scope.getClass() == scopeClass) {
+        scopesCollector.add((T)scope);
+      }
+    }
+    if (parentContext != null) {
+      parentContext.getScopes(
+        scopesCollector,
+        scopeClass,
+        parentContext.scopes.size());
+    }
+  }
+
+  public <T extends Output> BlazeContext addOutputSink(@NotNull Class<T> outputClass,
+                                               @NotNull OutputSink<T> outputSink) {
+    outputSinks.put(outputClass, outputSink);
+    return this;
+  }
+
+  /**
+   * Produces output by sending it to any registered sinks.
+   */
+  @SuppressWarnings("unchecked")
+  public synchronized <T extends Output> void output(@NotNull T output) {
+    Class<? extends Output> outputClass = output.getClass();
+    List<OutputSink<?>> outputSinks = this.outputSinks.get(outputClass);
+
+    boolean continuePropagation = true;
+    for (int i = outputSinks.size() - 1; i >= 0; --i) {
+      OutputSink<?> outputSink = outputSinks.get(i);
+      OutputSink.Propagation propagation = ((OutputSink<T>)outputSink).onOutput(output);
+      continuePropagation = propagation == OutputSink.Propagation.Continue;
+      if (!continuePropagation) {
+        break;
+      }
+    }
+    if (continuePropagation && parentContext != null) {
+      parentContext.output(output);
+    }
+  }
+
+  /**
+   * Sets the error state.
+   * <p/>
+   * <p>The error state will be propagated to any parents.
+   */
+  public void setHasError() {
+    this.hasErrors = true;
+  }
+
+  /**
+   * Returns true if there were errors
+   */
+  public boolean hasErrors() {
+    return hasErrors;
+  }
+
+  public boolean isRoot() {
+    return parentContext == null;
+  }
+
+  /**
+   * Returns true if no errors and isn't cancelled.
+   */
+  public boolean shouldContinue() {
+    return !hasErrors() && !isCancelled();
+  }
+
+  /**
+   * Sets whether errors are propagated to the parent context.
+   */
+  public void setPropagatesErrors(boolean propagatesErrors) {
+    this.propagatesErrors = propagatesErrors;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/BlazeScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/BlazeScope.java
new file mode 100644
index 0000000..56a48cd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/BlazeScope.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.scope;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A scoped facet of a scoped operation.
+ * <p/>
+ * <p>Attaches to a blaze context and starts and ends with it.
+ */
+public interface BlazeScope {
+  /**
+   * Called when the scope is added to the context.
+   */
+  void onScopeBegin(@NotNull BlazeContext context);
+
+  /**
+   * Called when the context scope is ending.
+   */
+  void onScopeEnd(@NotNull BlazeContext context);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/Output.java b/blaze-base/src/com/google/idea/blaze/base/scope/Output.java
new file mode 100644
index 0000000..c5a7759
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/Output.java
@@ -0,0 +1,22 @@
+/*
+ * 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.scope;
+
+/**
+ * A base interface for contextual output operations.
+ */
+public interface Output {
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/OutputSink.java b/blaze-base/src/com/google/idea/blaze/base/scope/OutputSink.java
new file mode 100644
index 0000000..df8e98e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/OutputSink.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.scope;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * An output sink registered with a context.
+ * <p/>
+ * <p>Register these via a ScopeExtension.
+ */
+public interface OutputSink<T extends Output> {
+  enum Propagation {
+    Continue,
+    Stop
+  }
+
+  /**
+   * Called when an Output of the correct type goes through the scope.
+   *
+   * @return Whether to continue propagation of this input.
+   */
+  Propagation onOutput(@NotNull T output);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/Result.java b/blaze-base/src/com/google/idea/blaze/base/scope/Result.java
new file mode 100644
index 0000000..7285a11
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/Result.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.scope;
+
+/**
+ * Helper class to be used when you want to return a result or error in a scoped function.
+ */
+public class Result<T> {
+  public final T result;
+  public final Throwable error;
+
+  public Result(T result) {
+    this.result = result;
+    this.error = null;
+  }
+
+  public Result(Throwable error) {
+    this.result = null;
+    this.error = error;
+  }
+
+  public static <T> Result<T> of(T result) {
+    return new Result<T>(result);
+  }
+
+  public static <T> Result<T> error(Throwable t) {
+    return new Result<T>(t);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/Scope.java b/blaze-base/src/com/google/idea/blaze/base/scope/Scope.java
new file mode 100644
index 0000000..6ddd243
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/Scope.java
@@ -0,0 +1,83 @@
+/*
+ * 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.scope;
+
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Helper methods to run scoped functions and operations in a scoped context.
+ */
+public final class Scope {
+  private static final Logger LOG = Logger.getInstance(Scope.class);
+
+  /**
+   * Runs a scoped function in a new root scope.
+   */
+  public static <T> T root(
+    @NotNull ScopedFunction<T> scopedFunction) {
+    return push(null, scopedFunction);
+  }
+
+  /**
+   * Runs a scoped function in a new nested scope.
+   */
+  public static <T> T push(
+    @Nullable BlazeContext parentContext,
+    @NotNull ScopedFunction<T> scopedFunction) {
+    BlazeContext context = new BlazeContext(parentContext);
+    try {
+      return scopedFunction.execute(context);
+    }
+    catch (RuntimeException e) {
+      context.setHasError();
+      LOG.error(e);
+      throw e;
+    }
+    finally {
+      context.endScope();
+    }
+  }
+
+  /**
+   * Runs a scoped operation in a new root scope.
+   */
+  public static void root(
+    @NotNull ScopedOperation scopedOperation) {
+    push(null, scopedOperation);
+  }
+
+  /**
+   * Runs a scoped operation in a new nested scope.
+   */
+  public static void push(
+    @Nullable BlazeContext parentContext,
+    @NotNull ScopedOperation scopedOperation) {
+    BlazeContext context = new BlazeContext(parentContext);
+    try {
+      scopedOperation.execute(context);
+    }
+    catch (RuntimeException e) {
+      context.setHasError();
+      LOG.error(e);
+      throw e;
+    }
+    finally {
+      context.endScope();
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/ScopedFunction.java b/blaze-base/src/com/google/idea/blaze/base/scope/ScopedFunction.java
new file mode 100644
index 0000000..8a780cd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/ScopedFunction.java
@@ -0,0 +1,25 @@
+/*
+ * 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.scope;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A scoped operation that can return a result to its caller.
+ */
+public interface ScopedFunction<T> {
+  T execute(@NotNull BlazeContext context);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/ScopedOperation.java b/blaze-base/src/com/google/idea/blaze/base/scope/ScopedOperation.java
new file mode 100644
index 0000000..29f3610
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/ScopedOperation.java
@@ -0,0 +1,25 @@
+/*
+ * 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.scope;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A scoped operation.
+ */
+public interface ScopedOperation {
+  void execute(@NotNull BlazeContext context);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/ScopedTask.java b/blaze-base/src/com/google/idea/blaze/base/scope/ScopedTask.java
new file mode 100644
index 0000000..341cae4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/ScopedTask.java
@@ -0,0 +1,51 @@
+/*
+ * 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.scope;
+
+import com.google.idea.blaze.base.scope.scopes.ProgressIndicatorScope;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Progressive;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Wrapper between an IntelliJ Task and a BlazeContext
+ */
+public abstract class ScopedTask implements Progressive {
+  @Nullable
+  final BlazeContext parentContext;
+
+  public ScopedTask() {
+    this(null /* parentContext */);
+  }
+
+  public ScopedTask(@Nullable BlazeContext parentContext) {
+    this.parentContext = parentContext;
+  }
+
+  @Override
+  public void run(@NotNull final ProgressIndicator indicator) {
+    Scope.push(parentContext, new ScopedOperation() {
+      @Override
+      public void execute(@NotNull BlazeContext context) {
+        context.push(new ProgressIndicatorScope(indicator));
+        ScopedTask.this.execute(context);
+      }
+    });
+  }
+
+  protected abstract void execute(@NotNull BlazeContext context);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java b/blaze-base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
new file mode 100644
index 0000000..e3b28d7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/output/IssueOutput.java
@@ -0,0 +1,176 @@
+/*
+ * 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.scope.output;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Output;
+import com.intellij.pom.Navigatable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+/**
+ * An issue in a blaze operation.
+ */
+public class IssueOutput implements Output {
+
+  public static final int NO_LINE = -1;
+  public static final int NO_COLUMN = -1;
+
+  @Nullable private final File file;
+  private final int line;
+  private final int column;
+  @NotNull private final Category category;
+  @NotNull private final String message;
+  @Nullable Navigatable navigatable;
+  @Nullable IssueData issueData;
+
+  public static class IssueData {}
+
+  public enum Category {
+    ERROR,
+    WARNING,
+    STATISTICS,
+    INFORMATION
+  }
+
+  @NotNull
+  public static Builder issue(@NotNull Category category, @NotNull String message) {
+    return new Builder(category, message);
+  }
+
+  @NotNull
+  public static Builder error(@NotNull String message) {
+    return new Builder(Category.ERROR, message);
+  }
+
+  @NotNull
+  public static Builder warn(@NotNull String message) {
+    return new Builder(Category.WARNING, message);
+  }
+
+  public static class Builder {
+    @NotNull private final Category category;
+    @NotNull private final String message;
+    @Nullable private File file;
+    private int line = NO_LINE;
+    private int column = NO_COLUMN;
+    @Nullable Navigatable navigatable;
+    @Nullable IssueData issueData;
+
+    public Builder(@NotNull Category category, @NotNull String message) {
+      this.category = category;
+      this.message = message;
+    }
+
+    @NotNull
+    public Builder inFile(@Nullable File file) {
+      this.file = file;
+      return this;
+    }
+
+    @NotNull
+    public Builder onLine(int line) {
+      this.line = line;
+      return this;
+    }
+
+    @NotNull
+    public Builder inColumn(int column) {
+      this.column = column;
+      return this;
+    }
+
+    @NotNull
+    public Builder withData(@Nullable IssueData issueData) {
+      this.issueData = issueData;
+      return this;
+    }
+
+    @NotNull
+    public Builder navigatable(@Nullable Navigatable navigatable) {
+      this.navigatable = navigatable;
+      return this;
+    }
+
+    public IssueOutput build() {
+      return new IssueOutput(file, line, column, navigatable, category, message, issueData);
+    }
+
+    public void submit(@NotNull BlazeContext context) {
+      context.output(build());
+      if (category == Category.ERROR) {
+        context.setHasError();
+      }
+    }
+  }
+
+  private IssueOutput(
+    @Nullable File file,
+    int line,
+    int column,
+    @Nullable Navigatable navigatable,
+    @NotNull Category category,
+    @NotNull String message,
+    @Nullable IssueData issueData) {
+    this.file = file;
+    this.line = line;
+    this.column = column;
+    this.navigatable = navigatable;
+    this.category = category;
+    this.message = message;
+    this.issueData = issueData;
+  }
+
+  @Nullable
+  public File getFile() {
+    return file;
+  }
+
+  public int getLine() {
+    return line;
+  }
+
+  public int getColumn() {
+    return column;
+  }
+
+  @Nullable
+  public Navigatable getNavigatable() {
+    return navigatable;
+  }
+
+  @NotNull
+  public Category getCategory() {
+    return category;
+  }
+
+  @NotNull
+  public String getMessage() {
+    return message;
+  }
+
+  @Override
+  public String toString() {
+    return message;
+  }
+
+  @Nullable
+  public IssueData getIssueData() {
+    return issueData;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/output/PerformanceWarning.java b/blaze-base/src/com/google/idea/blaze/base/scope/output/PerformanceWarning.java
new file mode 100644
index 0000000..1cdbf27
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/output/PerformanceWarning.java
@@ -0,0 +1,29 @@
+/*
+ * 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.scope.output;
+
+import com.google.idea.blaze.base.scope.Output;
+
+/**
+ * Output that is collected when running in performance collection mode.
+ */
+public class PerformanceWarning implements Output {
+  public final String text;
+
+  public PerformanceWarning(String text) {
+    this.text = text;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/output/PrintOutput.java b/blaze-base/src/com/google/idea/blaze/base/scope/output/PrintOutput.java
new file mode 100644
index 0000000..dfe0203
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/output/PrintOutput.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.scope.output;
+
+import com.google.idea.blaze.base.scope.Output;
+import com.intellij.execution.process.ProcessOutputTypes;
+import com.intellij.openapi.util.Key;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Output that can be printed to a log.
+ */
+public class PrintOutput implements Output {
+
+  @NotNull
+  private final String text;
+
+  @NotNull
+  private final OutputType outputType;
+
+  public enum OutputType {
+    NORMAL,
+    ERROR;
+
+    public static OutputType fromProcessOutputKey(Key outputKey) {
+      return outputKey == ProcessOutputTypes.STDERR ? ERROR : NORMAL;
+    }
+  }
+
+  public PrintOutput(@NotNull String text, @NotNull OutputType outputType) {
+    this.text = text;
+    this.outputType = outputType;
+  }
+
+  public PrintOutput(@NotNull String text) {
+    this(text, OutputType.NORMAL);
+  }
+
+  @NotNull
+  public String getText() {
+    return text;
+  }
+
+  @NotNull
+  public OutputType getOutputType() {
+    return outputType;
+  }
+
+  public static PrintOutput output(String text) {
+    return new PrintOutput(text);
+  }
+
+  public static PrintOutput error(String text) {
+    return new PrintOutput(text, OutputType.ERROR);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/output/StatusOutput.java b/blaze-base/src/com/google/idea/blaze/base/scope/output/StatusOutput.java
new file mode 100644
index 0000000..92ab9f3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/output/StatusOutput.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.scope.output;
+
+import com.google.idea.blaze.base.scope.Output;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Status message output.
+ */
+public class StatusOutput implements Output {
+  @NotNull
+  String status;
+
+  public StatusOutput(@NotNull String status) {
+    this.status = status;
+  }
+
+  @NotNull
+  public String getStatus() {
+    return status;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.java
new file mode 100644
index 0000000..646afbc
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/BlazeConsoleScope.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.scope.scopes;
+
+import com.google.idea.blaze.base.console.BlazeConsoleService;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.google.idea.blaze.base.scope.OutputSink;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.scope.output.PrintOutput.OutputType;
+import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.intellij.execution.ui.ConsoleViewContentType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Moves print output to the blaze console.
+ */
+public class BlazeConsoleScope implements BlazeScope {
+
+  public static class Builder {
+    private Project project;
+    private ProgressIndicator progressIndicator;
+    private boolean suppressConsole = false;
+
+    public Builder(@NotNull Project project) {
+      this(project, null);
+    }
+
+    public Builder(@NotNull Project project,
+                   ProgressIndicator progressIndicator) {
+      this.project = project;
+      this.progressIndicator = progressIndicator;
+    }
+
+    public Builder setSuppressConsole(boolean suppressConsole) {
+      this.suppressConsole = suppressConsole;
+      return this;
+    }
+
+    public BlazeConsoleScope build() {
+      return new BlazeConsoleScope(project, progressIndicator, suppressConsole);
+    }
+
+  }
+
+  @NotNull
+  private final Project project;
+
+  @NotNull
+  private final BlazeConsoleService blazeConsoleService;
+
+  @Nullable
+  private final ProgressIndicator progressIndicator;
+
+  private final boolean showDialogOnChange;
+  private boolean activated;
+
+  private OutputSink<PrintOutput> printSink = (output) -> {
+      @NotNull String text = output.getText();
+      @NotNull ConsoleViewContentType contentType = output.getOutputType() == OutputType.NORMAL
+                                                    ? ConsoleViewContentType.NORMAL_OUTPUT
+                                                    : ConsoleViewContentType.ERROR_OUTPUT;
+      print(text, contentType);
+      return OutputSink.Propagation.Continue;
+  };
+
+  private OutputSink<StatusOutput> statusSink = (output) -> {
+      @NotNull String text = output.getStatus();
+      @NotNull ConsoleViewContentType contentType = ConsoleViewContentType.NORMAL_OUTPUT;
+      print(text, contentType);
+      return OutputSink.Propagation.Continue;
+  };
+
+  private BlazeConsoleScope(@NotNull Project project,
+                           @Nullable ProgressIndicator progressIndicator,
+                           boolean suppressConsole) {
+    this.project = project;
+    this.blazeConsoleService = BlazeConsoleService.getInstance(project);
+    this.progressIndicator = progressIndicator;
+    this.showDialogOnChange = !suppressConsole;
+  }
+
+  private void print(String text, ConsoleViewContentType contentType) {
+    blazeConsoleService.print(text + "\n", contentType);
+
+    if (showDialogOnChange && !activated) {
+      activated = true;
+      ApplicationManager.getApplication().invokeLater(() -> blazeConsoleService.activateConsoleWindow());
+    }
+  }
+
+  @Override
+  public void onScopeBegin(@NotNull final BlazeContext context) {
+    context.addOutputSink(PrintOutput.class, printSink);
+    context.addOutputSink(StatusOutput.class, statusSink);
+    blazeConsoleService.clear();
+    blazeConsoleService.setStopHandler(() -> {
+      if (progressIndicator != null) {
+        progressIndicator.cancel();
+      }
+      context.setCancelled();
+    });
+  }
+
+  @Override
+  public void onScopeEnd(@NotNull BlazeContext context) {
+    blazeConsoleService.setStopHandler(null);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/scopes/IssuesScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/IssuesScope.java
new file mode 100644
index 0000000..5241fff
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/IssuesScope.java
@@ -0,0 +1,83 @@
+/*
+ * 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.scope.scopes;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.google.idea.blaze.base.scope.OutputSink;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.ui.BlazeProblemsView;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowManager;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.UUID;
+
+/**
+ * Shows the compiler output.
+ */
+public class IssuesScope implements BlazeScope, OutputSink<IssueOutput> {
+
+  private final Project project;
+  private final UUID sessionId;
+  private int issuesCount;
+
+  public IssuesScope(@NotNull Project project) {
+    this.project = project;
+    this.sessionId = UUID.randomUUID();
+  }
+
+  @Override
+  public void onScopeBegin(@NotNull BlazeContext context) {
+    context.addOutputSink(IssueOutput.class, this);
+    BlazeProblemsView blazeProblemsView = BlazeProblemsView.getInstance(project);
+    if (blazeProblemsView != null) {
+      blazeProblemsView.clearOldMessages(sessionId);
+    }
+  }
+
+  @Override
+  public void onScopeEnd(@NotNull BlazeContext context) {
+    if (issuesCount > 0) {
+      ApplicationManager.getApplication().invokeLater(new Runnable() {
+        @Override
+        public void run() {
+          focusProblemsView();
+        }
+      });
+    }
+  }
+
+  private void focusProblemsView() {
+    ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project);
+    ToolWindow toolWindow = toolWindowManager.getToolWindow("Problems");
+    if (toolWindow != null) {
+      toolWindow.activate(null, false, false);
+    }
+  }
+
+  @Override
+  public Propagation onOutput(@NotNull IssueOutput output) {
+    BlazeProblemsView blazeProblemsView = BlazeProblemsView.getInstance(project);
+    if (blazeProblemsView != null) {
+      blazeProblemsView.addMessage(output, sessionId);
+    }
+    ++issuesCount;
+    return Propagation.Continue;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/scopes/LoggedTimingScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/LoggedTimingScope.java
new file mode 100644
index 0000000..64fca5a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/LoggedTimingScope.java
@@ -0,0 +1,59 @@
+/*
+ * 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.scope.scopes;
+
+import com.google.common.base.Stopwatch;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.metrics.LoggingService;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.intellij.openapi.project.Project;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Timing scope where the results are sent to a logging service
+ */
+public class LoggedTimingScope implements BlazeScope {
+  // It is not guaranteed that the threading model will be sane during the entirety of this scope,
+  // so we use wall clock time and not ThreadMXBean where we could get user/system time.
+
+  Project project;
+  private final Action action;
+  private Stopwatch timer;
+
+  /**
+   * @param action The action we will be reporting a time for to the logging service
+   */
+  public LoggedTimingScope(Project project, Action action) {
+    this.project = project;
+    this.action = action;
+    this.timer = Stopwatch.createUnstarted();
+  }
+
+  @Override
+  public void onScopeBegin(BlazeContext context) {
+    timer.start();
+  }
+
+  @Override
+  public void onScopeEnd(BlazeContext context) {
+    if (!context.isCancelled()) {
+      long totalMS = timer.elapsed(TimeUnit.MILLISECONDS);
+      LoggingService.reportEvent(project, action, totalMS);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/scopes/NotificationScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/NotificationScope.java
new file mode 100644
index 0000000..96d67c7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/NotificationScope.java
@@ -0,0 +1,95 @@
+/*
+ * 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.scope.scopes;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+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 com.intellij.ui.SystemNotifications;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Notifies the user with a system notification when the scope ends.
+ */
+public class NotificationScope implements BlazeScope {
+
+  private static final long NOTIFICATION_THRESHOLD_MS = 0;
+
+  @NotNull
+  private Project project;
+
+  @NotNull
+  private final String notificationName;
+
+  @NotNull
+  private final String notificationTitle;
+
+  @NotNull
+  private final String notificationText;
+
+  @NotNull
+  private final String notificationErrorText;
+
+  private long startTime;
+
+  public NotificationScope(
+    @NotNull Project project,
+    @NotNull String notificationName,
+    @NotNull String notificationTitle,
+    @NotNull String notificationText,
+    @NotNull String notificationErrorText) {
+    this.project = project;
+    this.notificationName = notificationName;
+    this.notificationTitle = notificationTitle;
+    this.notificationText = notificationText;
+    this.notificationErrorText = notificationErrorText;
+  }
+
+  @Override
+  public void onScopeBegin(@NotNull BlazeContext context) {
+    startTime = System.currentTimeMillis();
+  }
+
+  @Override
+  public void onScopeEnd(@NotNull BlazeContext context) {
+    if (project.isDisposed()) {
+      return;
+    }
+    if (context.isCancelled()) {
+      context.output(new PrintOutput(notificationName + " cancelled"));
+      return;
+    }
+    long duration = System.currentTimeMillis() - startTime;
+    if (duration < NOTIFICATION_THRESHOLD_MS) {
+      return;
+    }
+
+    String notificationText = !context.hasErrors()
+                              ? this.notificationText
+                              : this.notificationErrorText;
+
+    SystemNotifications.getInstance().notify(
+      notificationName,
+      notificationTitle,
+      notificationText);
+
+    if (context.hasErrors()) {
+      context.output(new PrintOutput(notificationName + " failed", OutputType.ERROR));
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/scopes/PerformanceWarningScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/PerformanceWarningScope.java
new file mode 100644
index 0000000..e36d310
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/PerformanceWarningScope.java
@@ -0,0 +1,58 @@
+/*
+ * 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.scope.scopes;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.google.idea.blaze.base.scope.OutputSink;
+import com.google.idea.blaze.base.scope.output.PerformanceWarning;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+
+import java.util.List;
+
+/**
+ * Shows performance warnings.
+ */
+public class PerformanceWarningScope implements BlazeScope, OutputSink<PerformanceWarning> {
+
+  private final List<PerformanceWarning> outputs = Lists.newArrayList();
+
+  @Override
+  public void onScopeBegin(BlazeContext context) {
+    context.addOutputSink(PerformanceWarning.class, this);
+  }
+
+  @Override
+  public void onScopeEnd(BlazeContext context) {
+    if (outputs.isEmpty()) {
+      return;
+    }
+    context.output(new PrintOutput("\n===== PERFORMANCE WARNINGS =====\n"));
+    context.output(new PrintOutput("Your IDE isn't as fast as it could be."));
+    context.output(new PrintOutput("You can turn these off via Blaze > Show Performance Warnings."));
+    context.output(new PrintOutput(""));
+    for (PerformanceWarning output : outputs) {
+      context.output(new PrintOutput(output.text));
+    }
+  }
+
+  @Override
+  public Propagation onOutput(PerformanceWarning output) {
+    outputs.add(output);
+    return Propagation.Continue;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/scopes/ProgressIndicatorScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/ProgressIndicatorScope.java
new file mode 100644
index 0000000..3d1f96b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/ProgressIndicatorScope.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.scope.scopes;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.google.idea.blaze.base.scope.OutputSink;
+import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.util.AbstractProgressIndicatorExBase;
+import com.intellij.openapi.wm.ex.ProgressIndicatorEx;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Progress indicator scope.
+ * <p/>
+ * <p>Channels status outputs to the progress indicator text.
+ * <p>Cancels the scope if the user presses cancel on the progress indicator.
+ */
+public class ProgressIndicatorScope extends AbstractProgressIndicatorExBase
+  implements BlazeScope, OutputSink<StatusOutput> {
+
+  private final ProgressIndicator progressIndicator;
+  private BlazeContext context;
+
+  public ProgressIndicatorScope(@NotNull ProgressIndicator progressIndicator) {
+    this.progressIndicator = progressIndicator;
+
+    if (progressIndicator instanceof ProgressIndicatorEx) {
+      ((ProgressIndicatorEx)progressIndicator).addStateDelegate(this);
+    }
+  }
+
+  @Override
+  public void onScopeBegin(@NotNull BlazeContext context) {
+    this.context = context;
+    context.addOutputSink(StatusOutput.class, this);
+  }
+
+  @Override
+  public void onScopeEnd(@NotNull BlazeContext context) {
+  }
+
+  @Override
+  public void cancel() {
+    context.setCancelled();
+  }
+
+  @Override
+  public Propagation onOutput(@NotNull StatusOutput output) {
+    progressIndicator.setText(output.getStatus());
+    return Propagation.Continue;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/scopes/ProjectCloseScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/ProjectCloseScope.java
new file mode 100644
index 0000000..d4e8369
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/ProjectCloseScope.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.scope.scopes;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.project.ProjectManagerListener;
+import com.intellij.openapi.ui.Messages;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Prevents the user from closing the project while the scope is open.
+ */
+public class ProjectCloseScope implements ProjectManagerListener, BlazeScope {
+
+  @NotNull
+  private final Project project;
+
+  private boolean isApplicationExitingOrProjectClosing;
+
+  public ProjectCloseScope(@NotNull Project project) {
+    this.project = project;
+  }
+
+  @Override
+  public void onScopeBegin(@NotNull BlazeContext context) {
+    ProjectManager projectManager = ProjectManager.getInstance();
+    projectManager.addProjectManagerListener(project, this);
+  }
+
+  @Override
+  public void onScopeEnd(@NotNull BlazeContext context) {
+    ProjectManager projectManager = ProjectManager.getInstance();
+    projectManager.removeProjectManagerListener(project, this);
+  }
+
+  @Override
+  public void projectOpened(Project project) {
+  }
+
+  @Override
+  public boolean canCloseProject(Project project) {
+    if (!project.equals(this.project)) {
+      return true;
+    }
+    if (shouldPromptUser()) {
+      askUserToWait();
+      return false;
+    }
+    return false;
+  }
+
+  @Override
+  public void projectClosed(Project project) {
+  }
+
+  @Override
+  public void projectClosing(Project project) {
+    if (project.equals(this.project)) {
+      isApplicationExitingOrProjectClosing = true;
+    }
+  }
+
+  private boolean shouldPromptUser() {
+    return !isApplicationExitingOrProjectClosing;
+  }
+
+  private void askUserToWait() {
+    String buildSystem = Blaze.buildSystemName(project);
+    Messages.showMessageDialog(project,
+                               String.format("Please wait until %s command execution finishes", buildSystem),
+                               buildSystem + " Running",
+                               null);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java
new file mode 100644
index 0000000..49f0d24
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/scope/scopes/TimingScope.java
@@ -0,0 +1,123 @@
+/*
+ * 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.scope.scopes;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Prints timing information as output.
+ */
+public class TimingScope implements BlazeScope {
+
+  @NotNull
+  private final String name;
+
+  private long startTime;
+
+  private double duration;
+
+  @Nullable
+  private TimingScope parentScope;
+
+  @NotNull
+  private List<TimingScope> children = Lists.newArrayList();
+
+  public TimingScope(@NotNull String name) {
+    this.name = name;
+  }
+
+  @Override
+  public void onScopeBegin(@NotNull BlazeContext context) {
+    startTime = System.currentTimeMillis();
+    parentScope = context.getParentScope(this);
+
+    if (parentScope != null) {
+      parentScope.children.add(this);
+    }
+  }
+
+  @Override
+  public void onScopeEnd(@NotNull BlazeContext context) {
+    if (context.isCancelled()) {
+      return;
+    }
+
+    long elapsedTime = System.currentTimeMillis() - startTime;
+    duration = (double)elapsedTime / 1000.0;
+
+    if (parentScope == null) {
+      outputReport(context);
+    }
+  }
+
+  private void outputReport(@NotNull BlazeContext context) {
+    context.output(new PrintOutput("\n==== TIMING REPORT ====\n"));
+    outputReport(context, this, 0);
+  }
+
+  private static void outputReport(
+    @NotNull BlazeContext context,
+    @NotNull TimingScope timingScope,
+    int depth) {
+    String selfString = "";
+
+    // Self time trivially 100% if no children
+    if (timingScope.children.size() > 0) {
+      // Calculate self time as <my duration> - <sum child duration>
+      double selfTime = timingScope.duration;
+      for (TimingScope child : timingScope.children) {
+        selfTime -= child.duration;
+      }
+
+      selfString = selfTime > 0.1
+                   ? String.format(" (%s)", durationStr(selfTime))
+                   : "";
+    }
+
+    context.output(new PrintOutput(
+      String.format("%s%s: %s%s",
+                    getIndentation(depth),
+                    timingScope.name,
+                    durationStr(timingScope.duration),
+                    selfString)
+    ));
+
+    for (TimingScope child : timingScope.children) {
+      outputReport(context, child, depth + 1);
+    }
+  }
+
+  private static String durationStr(double time) {
+    return time >= 1.0
+           ? String.format("%.1fs", time)
+           : String.format("%dms", (int)(time * 1000));
+  }
+
+  private static String getIndentation(int depth) {
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < depth; ++i) {
+      sb.append("    ");
+    }
+    return sb.toString();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/Blaze.java b/blaze-base/src/com/google/idea/blaze/base/settings/Blaze.java
new file mode 100644
index 0000000..2b85c24
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/Blaze.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings;
+
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.intellij.ide.DataManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * Blaze project utilities.
+ */
+public class Blaze {
+
+  public enum BuildSystem {
+    Blaze,
+    Bazel;
+
+    /**
+     * The build system name, capitalized.
+     */
+    public String getName() {
+      return name();
+    }
+
+    /**
+     * The build system name, capitalized.
+     */
+    public String getLowerCaseName() {
+      return name().toLowerCase();
+    }
+  }
+
+  private Blaze() {
+  }
+
+  public static boolean isBlazeProjectOpen() {
+    for (Project project : ProjectManager.getInstance().getOpenProjects()) {
+      if (isBlazeProject(project)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Returns whether this project was imported from blaze.
+   */
+  public static boolean isBlazeProject(Project project) {
+    return BlazeImportSettingsManager.getInstance(project).getImportSettings() != null;
+  }
+
+  /**
+   * Returns the build system associated with this project,
+   * or falls back to the default blaze build system if the project is null or
+   * not a blaze project.
+   */
+  public static BuildSystem getBuildSystem(@Nullable Project project) {
+    BlazeImportSettings importSettings =
+      project == null ? null: BlazeImportSettingsManager.getInstance(project).getImportSettings();
+    if (importSettings == null) {
+      return BuildSystemProvider.defaultBuildSystem().buildSystem();
+    }
+    return importSettings.getBuildSystem();
+  }
+
+  /**
+   * The name of the build system associated with the given project,
+   * or falls back to the default blaze build system if the project is null or
+   * not a blaze project.
+   */
+  public static String buildSystemName(@Nullable Project project) {
+    return getBuildSystem(project).getName();
+  }
+
+  /**
+   * The name of the application-wide build system default. This should
+   * only be used in situations where it doesn't make sense to use the build system
+   * associated with the current project (e.g. the import project action).
+   */
+  public static String defaultBuildSystemName() {
+    return BuildSystemProvider.defaultBuildSystem().buildSystem().getName();
+  }
+
+  /**
+   * Tries to guess the current project, and uses that to determine the build system name.<br>
+   * Should only be used in situations where the current project is not accessible.
+   */
+  public static String guessBuildSystemName() {
+    Project project = guessCurrentProject();
+    return buildSystemName(project);
+  }
+
+  private static Project guessCurrentProject() {
+    Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
+    if (openProjects.length == 1) {
+      return openProjects[0];
+    }
+    if (SwingUtilities.isEventDispatchThread()) {
+      return (Project) DataManager.getInstance().getDataContext().getData("project");
+    }
+    return null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java b/blaze-base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java
new file mode 100644
index 0000000..ac4db38
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/BlazeImportSettings.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings;
+
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.util.xmlb.annotations.Tag;
+
+import javax.annotation.Nullable;
+
+/**
+ * Project settings that are set at import time.
+ */
+@Tag("BlazeProjectSettings") // Legacy migration support
+public final class BlazeImportSettings {
+
+  private String workspaceRoot = "";
+
+  private String projectName = "";
+
+  private String projectDataDirectory = "";
+
+  private String locationHash = "";
+
+  private String projectViewFile;
+
+  private BuildSystem buildSystem = BuildSystem.Blaze; // default for backwards compatibility with existing projects.
+
+  // Used by bean serialization
+  @SuppressWarnings("unused")
+  BlazeImportSettings() {
+  }
+
+  public BlazeImportSettings(
+    String workspaceRoot,
+    String projectName,
+    String projectDataDirectory,
+    String locationHash,
+    String projectViewFile,
+    BuildSystem buildSystem) {
+    this.workspaceRoot = workspaceRoot;
+    this.projectName = projectName;
+    this.projectDataDirectory = projectDataDirectory;
+    this.locationHash = locationHash;
+    this.projectViewFile = projectViewFile;
+    this.buildSystem = buildSystem;
+  }
+
+  @SuppressWarnings("unused")
+  public String getWorkspaceRoot() {
+    return workspaceRoot;
+  }
+
+  @SuppressWarnings("unused")
+  public String getProjectName() {
+    return projectName;
+  }
+
+  @SuppressWarnings("unused")
+  public String getProjectDataDirectory() {
+    return projectDataDirectory;
+  }
+
+  /**
+   * Hash used to give the project a unique directory in the system directory.
+   */
+  @SuppressWarnings("unused")
+  public String getLocationHash() {
+    return locationHash;
+  }
+
+  /**
+   * The user's local project view file
+   */
+  @SuppressWarnings("unused")
+  public String getProjectViewFile() {
+    return projectViewFile;
+  }
+
+  /**
+   * The build system used for the project.
+   */
+  @SuppressWarnings("unused")
+  public BuildSystem getBuildSystem() {
+    return buildSystem;
+  }
+
+  // Used by bean serialization
+  @SuppressWarnings("unused")
+  public void setWorkspaceRoot(String workspaceRoot) {
+    this.workspaceRoot = workspaceRoot;
+  }
+
+  // Used by bean serialization
+  @SuppressWarnings("unused")
+  public void setProjectName(String projectName) {
+    this.projectName = projectName;
+  }
+
+  // Used by bean serialization
+  @SuppressWarnings("unused")
+  public void setProjectDataDirectory(String projectDataDirectory) {
+    this.projectDataDirectory = projectDataDirectory;
+  }
+
+  // Used by bean serialization
+  @SuppressWarnings("unused")
+  public void setLocationHash(String locationHash) {
+    this.locationHash = locationHash;
+  }
+
+  // Used by bean serialization
+  @SuppressWarnings("unused")
+  public void setProjectViewFile(@Nullable String projectViewFile) {
+    this.projectViewFile = projectViewFile;
+  }
+
+  // Used by bean serialization -- legacy import support
+  @SuppressWarnings("unused")
+  public void setAsProjectFile(@Nullable String projectViewFile) {
+    this.projectViewFile = projectViewFile;
+  }
+
+  // Used by bean serialization
+  @SuppressWarnings("unused")
+  public void setBuildSystem(BuildSystem buildSystem) {
+    this.buildSystem = buildSystem;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java b/blaze-base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java
new file mode 100644
index 0000000..3e40500
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/BlazeImportSettingsManager.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings;
+
+import com.google.common.collect.Lists;
+import com.intellij.openapi.components.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.util.xmlb.annotations.AbstractCollection;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Manages storage for the project's {@link BlazeImportSettings}.
+ */
+@State(
+  name = "BlazeSettings",
+  storages = {@Storage(file = StoragePathMacros.PROJECT_FILE),
+    @Storage(file = StoragePathMacros.PROJECT_CONFIG_DIR + "/blaze.xml",
+      scheme = StorageScheme.DIRECTORY_BASED)}
+)
+public class BlazeImportSettingsManager implements
+                                        PersistentStateComponent<BlazeImportSettingsManager.State> {
+
+  @Nullable
+  private BlazeImportSettings importSettings;
+
+  @NotNull
+  private Project project;
+
+  public BlazeImportSettingsManager(@NotNull Project project) {
+    this.project = project;
+  }
+
+  @NotNull
+  public static BlazeImportSettingsManager getInstance(@NotNull Project project) {
+    return ServiceManager.getService(project, BlazeImportSettingsManager.class);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Nullable
+  @Override
+  public State getState() {
+    State state = new State();
+    List<BlazeImportSettings> value = Lists.newArrayList();
+    if (importSettings != null) {
+      value.add(importSettings);
+    }
+    state.setLinkedExternalProjectsSettings(value);
+    return state;
+  }
+
+  @Override
+  public void loadState(State state) {
+    Collection<BlazeImportSettings> settings = state.getLinkedExternalProjectsSettings();
+    if (settings != null && !settings.isEmpty()) {
+      importSettings = settings.iterator().next();
+    }
+    else {
+      importSettings = null;
+    }
+  }
+
+  @Nullable
+  public BlazeImportSettings getImportSettings() {
+    return importSettings;
+  }
+
+  public void setImportSettings(@NotNull BlazeImportSettings importSettings) {
+    this.importSettings = importSettings;
+  }
+
+  /**
+   * State class for the Blaze settings.
+   */
+  static class State {
+
+    private List<BlazeImportSettings> importSettings = Lists.newArrayList();
+
+    @AbstractCollection(surroundWithTag = false, elementTypes = {BlazeImportSettings.class})
+    public List<BlazeImportSettings> getLinkedExternalProjectsSettings() {
+      return importSettings;
+    }
+
+    public void setLinkedExternalProjectsSettings(List<BlazeImportSettings> settings) {
+      importSettings = settings;
+    }
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java b/blaze-base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java
new file mode 100644
index 0000000..dc3b699
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/BlazeUserSettings.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings;
+
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileTypeFactory;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatus;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.util.xmlb.XmlSerializerUtil;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+
+/**
+ * Stores blaze view settings.
+ */
+@State(
+  name = "BlazeUserSettings",
+  storages = @Storage(id = "other", value = "blaze.view.xml")
+)
+public class BlazeUserSettings implements PersistentStateComponent<BlazeUserSettings> {
+
+  public boolean suppressConsoleForRunAction = false;
+  private boolean resyncAutomatically = false;
+  private boolean buildFileSupportEnabled = true;
+  private boolean syncStatusPopupShown = false;
+  private boolean expandSyncToWorkingSet = true;
+  private boolean showPerformanceWarnings = false;
+  private boolean attachSourcesByDefault = false;
+  private boolean attachSourcesOnDemand = false;
+  private boolean collapseProjectView = true;
+  private String blazeBinaryPath = "/usr/bin/blaze";
+  @Nullable private String bazelBinaryPath;
+
+  @NotNull
+  public static BlazeUserSettings getInstance() {
+    return ServiceManager.getService(BlazeUserSettings.class);
+  }
+
+  @Override
+  @NotNull
+  public BlazeUserSettings getState() {
+    return this;
+  }
+
+  @Override
+  public void loadState(BlazeUserSettings state) {
+    XmlSerializerUtil.copyBean(state, this);
+  }
+
+  /**
+   * Also kicks off an incremental sync if we're now syncing automatically,
+   * and the project is currently dirty.
+   */
+  public void setResyncAutomatically(boolean resyncAutomatically) {
+    if (this.resyncAutomatically == resyncAutomatically) {
+      return;
+    }
+    this.resyncAutomatically = resyncAutomatically;
+    ProjectManager projectManager = ApplicationManager.getApplication().getComponent(ProjectManager.class);
+    Project[] openProjects = projectManager.getOpenProjects();
+    for (Project project : openProjects) {
+      if (Blaze.isBlazeProject(project)) {
+        BlazeSyncStatus.getInstance(project).queueAutomaticSyncIfDirty();
+      }
+    }
+  }
+
+  public boolean getResyncAutomatically() {
+    return resyncAutomatically;
+  }
+
+  /**
+   * Triggers an update to the list of registered file types.
+   */
+  public void setBuildFileSupportEnabled(boolean supportEnabled) {
+    if (supportEnabled != buildFileSupportEnabled) {
+      buildFileSupportEnabled = supportEnabled;
+      BuildFileTypeFactory.updateBuildFileLanguageEnabled(BuildFileLanguage.BUILD_FILE_SUPPORT_ENABLED.getValue() && supportEnabled);
+    }
+  }
+
+  public boolean getBuildFileSupportEnabled() {
+    return buildFileSupportEnabled;
+  }
+
+  public boolean getSuppressConsoleForRunAction() {
+    return suppressConsoleForRunAction;
+  }
+
+  public void setSuppressConsoleForRunAction(boolean suppressConsoleForRunAction) {
+    this.suppressConsoleForRunAction = suppressConsoleForRunAction;
+  }
+
+  public boolean getSyncStatusPopupShown() {
+    return syncStatusPopupShown;
+  }
+
+  public void setSyncStatusPopupShown(boolean syncStatusPopupShown) {
+    this.syncStatusPopupShown = syncStatusPopupShown;
+  }
+
+  public boolean getExpandSyncToWorkingSet() {
+    return expandSyncToWorkingSet;
+  }
+
+  public void setExpandSyncToWorkingSet(boolean expandSyncToWorkingSet) {
+    this.expandSyncToWorkingSet = expandSyncToWorkingSet;
+  }
+
+  public boolean getShowPerformanceWarnings() {
+    return showPerformanceWarnings;
+  }
+
+  public void setShowPerformanceWarnings(boolean showPerformanceWarnings) {
+    this.showPerformanceWarnings = showPerformanceWarnings;
+  }
+
+  public String getBlazeBinaryPath() {
+    return blazeBinaryPath;
+  }
+
+  public void setBlazeBinaryPath(String blazeBinaryPath) {
+    this.blazeBinaryPath = blazeBinaryPath;
+  }
+
+  @Nullable
+  public String getBazelBinaryPath() {
+    return bazelBinaryPath;
+  }
+
+  public void setBazelBinaryPath(String bazelBinaryPath) {
+    this.bazelBinaryPath = bazelBinaryPath;
+  }
+
+  public boolean getAttachSourcesByDefault() {
+    return attachSourcesByDefault;
+  }
+
+  public void setAttachSourcesByDefault(boolean attachSourcesByDefault) {
+    this.attachSourcesByDefault = attachSourcesByDefault;
+  }
+
+  public boolean getAttachSourcesOnDemand() {
+    return attachSourcesOnDemand;
+  }
+
+  public void setAttachSourcesOnDemand(boolean attachSourcesOnDemand) {
+    this.attachSourcesOnDemand = attachSourcesOnDemand;
+  }
+
+  public boolean getCollapseProjectView() {
+    return collapseProjectView;
+  }
+
+  public void setCollapseProjectView(boolean collapseProjectView) {
+    this.collapseProjectView = collapseProjectView;
+  }
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/IsBlazeProjectCondition.java b/blaze-base/src/com/google/idea/blaze/base/settings/IsBlazeProjectCondition.java
new file mode 100644
index 0000000..ebfc6a7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/IsBlazeProjectCondition.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Condition;
+
+/**
+ * Condition for enabling features (e.g. Blaze and Crow consoles) only in Blaze projects.
+ */
+public class IsBlazeProjectCondition implements Condition<Project> {
+
+  @Override
+  public boolean value(Project project) {
+    return project != null && Blaze.isBlazeProject(project);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java b/blaze-base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java
new file mode 100644
index 0000000..d63c2cf
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/ui/BlazeUserSettingsConfigurable.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings.ui;
+
+import com.google.common.base.Objects;
+import com.google.idea.blaze.base.lang.buildfile.language.BuildFileLanguage;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatusImpl;
+import com.google.idea.blaze.base.ui.FileSelectorWithStoredHistory;
+import com.intellij.openapi.options.BaseConfigurable;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.options.SearchableConfigurable;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.uiDesigner.core.GridConstraints;
+import com.intellij.uiDesigner.core.GridLayoutManager;
+import com.intellij.uiDesigner.core.Spacer;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Blaze console view settings
+ */
+public class BlazeUserSettingsConfigurable extends BaseConfigurable implements SearchableConfigurable {
+
+  private static final String BLAZE_BINARY_PATH_KEY = "blaze.binary.path";
+  public static final String BAZEL_BINARY_PATH_KEY = "bazel.binary.path";
+
+  private final BuildSystem buildSystem;
+
+  private JPanel myMainPanel;
+  private JCheckBox suppressConsoleForRunAction;
+  private JCheckBox resyncAutomatically;
+  private JCheckBox buildFileSupportEnabled;
+  private JCheckBox attachSourcesByDefault;
+  private JCheckBox attachSourcesOnDemand;
+  private JCheckBox collapseProjectView;
+  private FileSelectorWithStoredHistory blazeBinaryPathField;
+  private FileSelectorWithStoredHistory bazelBinaryPathField;
+
+  public BlazeUserSettingsConfigurable(Project project) {
+    this.buildSystem = Blaze.getBuildSystem(project);
+    setupUI();
+  }
+
+  @Override
+  public String getDisplayName() {
+    return buildSystem.getName() + " View Settings";
+  }
+
+  @Nullable
+  @Override
+  public String getHelpTopic() {
+    return null;
+  }
+
+  @Override
+  public void apply() throws ConfigurationException {
+    BlazeUserSettings settings = BlazeUserSettings.getInstance();
+    settings.setSuppressConsoleForRunAction(suppressConsoleForRunAction.isSelected());
+    settings.setResyncAutomatically(resyncAutomatically.isSelected());
+    settings.setBuildFileSupportEnabled(buildFileSupportEnabled.isSelected());
+    settings.setAttachSourcesByDefault(attachSourcesByDefault.isSelected());
+    settings.setAttachSourcesOnDemand(attachSourcesOnDemand.isSelected());
+    settings.setCollapseProjectView(collapseProjectView.isSelected());
+    if (blazeBinaryPathField.getText() != null) {
+      settings.setBlazeBinaryPath(blazeBinaryPathField.getText());
+    }
+    if (bazelBinaryPathField.getText() != null) {
+      settings.setBazelBinaryPath(bazelBinaryPathField.getText());
+    }
+  }
+
+  @Override
+  public void reset() {
+    BlazeUserSettings settings = BlazeUserSettings.getInstance();
+    suppressConsoleForRunAction.setSelected(settings.getSuppressConsoleForRunAction());
+    resyncAutomatically.setSelected(settings.getResyncAutomatically());
+    buildFileSupportEnabled.setSelected(settings.getBuildFileSupportEnabled());
+    attachSourcesByDefault.setSelected(settings.getAttachSourcesByDefault());
+    attachSourcesOnDemand.setSelected(settings.getAttachSourcesOnDemand());
+    collapseProjectView.setSelected(settings.getCollapseProjectView());
+    blazeBinaryPathField.setTextWithHistory(settings.getBlazeBinaryPath());
+    bazelBinaryPathField.setTextWithHistory(settings.getBazelBinaryPath());
+  }
+
+  @Nullable
+  @Override
+  public JComponent createComponent() {
+    resyncAutomatically.setVisible(BlazeSyncStatusImpl.AUTOMATIC_INCREMENTAL_SYNC.getValue());
+    buildFileSupportEnabled.setVisible(BuildFileLanguage.BUILD_FILE_SUPPORT_ENABLED.getValue());
+    return myMainPanel;
+  }
+
+  @Override
+  public boolean isModified() {
+    BlazeUserSettings settings = BlazeUserSettings.getInstance();
+    return !Objects.equal(suppressConsoleForRunAction.isSelected(), settings.getSuppressConsoleForRunAction()) ||
+           !Objects.equal(resyncAutomatically.isSelected(), settings.getResyncAutomatically()) ||
+           !Objects.equal(buildFileSupportEnabled.isSelected(), settings.getBuildFileSupportEnabled()) ||
+           !Objects.equal(attachSourcesByDefault.isSelected(), settings.getAttachSourcesByDefault()) ||
+           !Objects.equal(attachSourcesOnDemand.isSelected(), settings.getAttachSourcesOnDemand()) ||
+           !Objects.equal(collapseProjectView.isSelected(), settings.getCollapseProjectView()) ||
+           !Objects.equal(blazeBinaryPathField.getText(), settings.getBlazeBinaryPath()) ||
+           !Objects.equal(bazelBinaryPathField.getText(), settings.getBazelBinaryPath());
+  }
+
+  @Override
+  public void disposeUIResources() {
+  }
+
+  @Override
+  public String getId() {
+    return "blaze.view.settings";
+  }
+
+  @Nullable
+  @Override
+  public Runnable enableSearch(String option) {
+    return null;
+  }
+
+
+  /**
+   * Initially generated by IntelliJ from a .form file.
+   */
+  private void setupUI() {
+    myMainPanel = new JPanel();
+    myMainPanel.setLayout(new GridLayoutManager(8, 2, new Insets(0, 0, 0, 0), -1, -1));
+    suppressConsoleForRunAction = new JCheckBox();
+    suppressConsoleForRunAction.setText(String.format("Suppress %s console for Run/Debug actions", buildSystem));
+    suppressConsoleForRunAction.setVerticalAlignment(0);
+    myMainPanel.add(suppressConsoleForRunAction,
+                    new GridConstraints(0, 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));
+    resyncAutomatically = new JCheckBox();
+    resyncAutomatically.setSelected(false);
+    resyncAutomatically.setText("Automatically re-sync project when BUILD files change");
+    myMainPanel.add(resyncAutomatically, new GridConstraints(1, 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));
+    buildFileSupportEnabled = new JCheckBox();
+    buildFileSupportEnabled.setSelected(true);
+    buildFileSupportEnabled.setText("BUILD file language support enabled");
+    myMainPanel.add(buildFileSupportEnabled, new GridConstraints(2, 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));
+    attachSourcesByDefault = new JCheckBox();
+    attachSourcesByDefault.setSelected(false);
+    attachSourcesByDefault.setText("Automatically attach sources on project sync (WARNING: increases index time by 100%+)");
+    myMainPanel.add(attachSourcesByDefault, new GridConstraints(3, 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));
+    attachSourcesByDefault.addActionListener((event) -> {
+      BlazeUserSettings settings = BlazeUserSettings.getInstance();
+      if (attachSourcesByDefault.isSelected() && !settings.getAttachSourcesByDefault()) {
+        int result = Messages.showOkCancelDialog(
+          "You are turning on source jars by default. This setting increases indexing time by "
+          + ">100%, can cost ~1GB RAM, and will increase project reopen time significantly. "
+          + "Are you sure you want to proceed?",
+          "Turn On Sources By Default?",
+          null
+        );
+        if (result != Messages.OK) {
+          attachSourcesByDefault.setSelected(false);
+        }
+      }
+    });
+
+    attachSourcesOnDemand = new JCheckBox();
+    attachSourcesOnDemand.setSelected(false);
+    attachSourcesOnDemand.setText("Automatically attach sources when you open decompiled source");
+    myMainPanel.add(attachSourcesOnDemand, new GridConstraints(4, 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));
+    collapseProjectView = new JCheckBox();
+    collapseProjectView.setSelected(false);
+    collapseProjectView.setText("Collapse project view directory roots");
+    myMainPanel.add(collapseProjectView, new GridConstraints(5, 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));
+
+    blazeBinaryPathField = FileSelectorWithStoredHistory.create(BLAZE_BINARY_PATH_KEY, "Specify the blaze binary path");
+    bazelBinaryPathField = FileSelectorWithStoredHistory.create(BAZEL_BINARY_PATH_KEY, "Specify the bazel binary path");
+
+    JLabel pathLabel;
+    JComponent pathPanel;
+    if (buildSystem == BuildSystem.Blaze) {
+      pathPanel = blazeBinaryPathField;
+      pathLabel = new JLabel("Blaze binary location");
+    } else {
+      pathPanel = bazelBinaryPathField;
+      pathLabel = new JLabel("Bazel binary location");
+    }
+    pathLabel.setLabelFor(pathPanel);
+    myMainPanel.add(pathLabel, new GridConstraints(6, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE,
+                                                   GridConstraints.SIZEPOLICY_FIXED,
+                                                   GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+    myMainPanel.add(pathPanel, new GridConstraints(6, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL,
+                                                   GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW,
+                                                   GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false));
+
+    myMainPanel.add(new Spacer(), new GridConstraints(7, 0, 1, 2, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_VERTICAL, 1,
+                                                 GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false));
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/ui/EditProjectViewAction.java b/blaze-base/src/com/google/idea/blaze/base/settings/ui/EditProjectViewAction.java
new file mode 100644
index 0000000..73259f2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/ui/EditProjectViewAction.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings.ui;
+
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.fileEditor.FileEditorManager;
+import com.intellij.openapi.fileEditor.OpenFileDescriptor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import java.io.File;
+
+/**
+ * Opens all the user's project views.
+ */
+public class EditProjectViewAction extends BlazeAction {
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project == null) {
+      return;
+    }
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet == null) {
+      return;
+    }
+    for (ProjectViewSet.ProjectViewFile projectViewFile : projectViewSet.getProjectViewFiles()) {
+      File file = projectViewFile.projectViewFile;
+      if (file != null) {
+        VirtualFile virtualFile = VfsUtil.findFileByIoFile(file, true);
+        if (virtualFile != null) {
+          OpenFileDescriptor descriptor = new OpenFileDescriptor(project, virtualFile);
+          FileEditorManager.getInstance(project).openTextEditor(descriptor, true);
+        }
+      }
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/ui/JPanelProvidingProject.java b/blaze-base/src/com/google/idea/blaze/base/settings/ui/JPanelProvidingProject.java
new file mode 100644
index 0000000..f29c7db
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/ui/JPanelProvidingProject.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings.ui;
+
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataProvider;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * A normal JPanel which implements DataProvider, providing the specified IntelliJ Project.<p>
+ * This is used by IntelliJ's action system to determine the relevant project associated with
+ * a UI component.
+ */
+public class JPanelProvidingProject extends JPanel implements DataProvider {
+
+  private final Project project;
+
+  public JPanelProvidingProject(Project project, LayoutManager layoutManager) {
+    super(layoutManager);
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public Object getData(String dataId) {
+    return CommonDataKeys.PROJECT.is(dataId) ? project : null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java b/blaze-base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
new file mode 100644
index 0000000..4b78ff9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/settings/ui/ProjectViewUi.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.settings.ui;
+
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewFileType;
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
+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.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.scope.OutputSink;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverProvider;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.ide.DataManager;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.Result;
+import com.intellij.openapi.application.WriteAction;
+import com.intellij.openapi.command.undo.UndoUtil;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.EditorFactory;
+import com.intellij.openapi.editor.EditorSettings;
+import com.intellij.openapi.editor.colors.EditorColors;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.editor.impl.DocumentImpl;
+import com.intellij.openapi.editor.impl.EditorFactoryImpl;
+import com.intellij.openapi.fileEditor.impl.FileDocumentManagerImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.project.ProjectManager;
+import com.intellij.openapi.project.impl.ProjectImpl;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.impl.PsiManagerEx;
+import com.intellij.psi.impl.file.impl.FileManager;
+import com.intellij.testFramework.LightVirtualFile;
+import com.intellij.ui.components.JBLabel;
+import org.jetbrains.annotations.NotNull;
+import org.picocontainer.MutablePicoContainer;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.util.List;
+
+/**
+ * UI for changing the ProjectView.
+ */
+public class ProjectViewUi {
+
+  private static final String USE_SHARED_PROJECT_VIEW = "Use shared project view file";
+
+  private final Disposable parentDisposable;
+  private EditorEx projectViewEditor;
+  private JCheckBox useShared;
+
+  private WorkspaceRoot workspaceRoot;
+  private boolean useSharedProjectView;
+  private boolean allowEditShared;
+  private String sharedProjectViewText;
+  private boolean settingsInitialized;
+
+  public ProjectViewUi(Disposable parentDisposable) {
+    this.parentDisposable = parentDisposable;
+  }
+
+  /**
+   * To support the custom language features, we need a ProjectImpl, and it's not desirable to create one from scratch.<br>
+   * @return the current, non-default project, if one exists, else the default project.
+   */
+  public static Project getProject() {
+    Project project = (Project) DataManager.getInstance().getDataContext().getData("project");
+    if (project != null && project instanceof ProjectImpl) {
+      return project;
+    }
+    return ProjectManager.getInstance().getDefaultProject();
+  }
+
+  public static Dimension getMinimumSize() {
+    return new Dimension(1000, 550);
+  }
+
+  private static EditorEx createEditor(String tooltip) {
+    Project project = getProject();
+    LightVirtualFile virtualFile = new LightVirtualFile("mockProjectViewFile", ProjectViewLanguage.INSTANCE, "");
+    final Document document = ((EditorFactoryImpl) EditorFactory.getInstance()).createDocument(true);
+    ((DocumentImpl) document).setAcceptSlashR(true);
+    FileDocumentManagerImpl.registerDocument(document, virtualFile);
+
+    FileManager fileManager = ((PsiManagerEx) PsiManager.getInstance(project)).getFileManager();
+    fileManager.setViewProvider(virtualFile, fileManager.createFileViewProvider(virtualFile, true));
+
+    if (project.isDefault()) {
+      // Undo-redo doesn't work with the default project. Explicitly turn it off to avoid error dialogs.
+      UndoUtil.disableUndoFor(document);
+    }
+
+    EditorEx editor = (EditorEx) EditorFactory.getInstance().createEditor(document, project, ProjectViewFileType.INSTANCE, false);
+    final EditorSettings settings = editor.getSettings();
+    settings.setLineNumbersShown(false);
+    settings.setLineMarkerAreaShown(false);
+    settings.setFoldingOutlineShown(false);
+    settings.setRightMarginShown(false);
+    settings.setAdditionalPageAtBottom(false);
+    editor.getComponent().setMinimumSize(getMinimumSize());
+    editor.getComponent().setPreferredSize(getMinimumSize());
+    editor.getComponent().setToolTipText(tooltip);
+    editor.getComponent().setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null);
+    editor.getComponent().setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null);
+    return editor;
+  }
+
+  public void fillUi(JPanel canvas, int indentLevel) {
+    String tooltip = "Enter a project view descriptor file. See 'go/intellij/docs/project-views.md' for more information.";
+
+    projectViewEditor = createEditor(tooltip);
+    projectViewEditor.getColorsScheme().setColor(EditorColors.READONLY_BACKGROUND_COLOR, UIManager.getColor("Label.background"));
+    Disposer.register(parentDisposable, () -> EditorFactory.getInstance().releaseEditor(projectViewEditor));
+
+    JBLabel labelsLabel = new JBLabel("Project View");
+    labelsLabel.setToolTipText(tooltip);
+    canvas.add(labelsLabel, UiUtil.getFillLineConstraints(indentLevel));
+
+    canvas.add(projectViewEditor.getComponent(), UiUtil.getFillLineConstraints(indentLevel));
+
+    useShared = new JCheckBox(USE_SHARED_PROJECT_VIEW);
+    useShared.addActionListener(e -> {
+      useSharedProjectView = useShared.isSelected();
+      if (useSharedProjectView) {
+        setProjectViewText(sharedProjectViewText);
+      }
+      updateTextAreasEnabled();
+    });
+    canvas.add(useShared, UiUtil.getFillLineConstraints(indentLevel));
+  }
+
+  public void init(
+    WorkspaceRoot workspaceRoot,
+    String projectViewText,
+    @Nullable String sharedProjectViewText,
+    @Nullable File sharedProjectViewFile,
+    boolean useSharedProjectView,
+    boolean allowEditShared
+  ) {
+    this.workspaceRoot = workspaceRoot;
+    this.useSharedProjectView = useSharedProjectView;
+    this.allowEditShared = allowEditShared;
+    this.sharedProjectViewText = sharedProjectViewText;
+
+    assert !(useSharedProjectView && sharedProjectViewText == null);
+
+    if (sharedProjectViewFile != null) {
+      WorkspacePath workspacePath = workspaceRoot.workspacePathFor(sharedProjectViewFile);
+      useShared.setText(USE_SHARED_PROJECT_VIEW + ": " + workspacePath.relativePath());
+    }
+
+    useShared.setSelected(useSharedProjectView);
+
+    if (sharedProjectViewText == null) {
+      useShared.setEnabled(false);
+    }
+
+    setDummyWorkspacePathResolverProvider(workspaceRoot);
+    setProjectViewText(projectViewText);
+    settingsInitialized = true;
+  }
+
+  private void setDummyWorkspacePathResolverProvider(WorkspaceRoot workspaceRoot) {
+    MutablePicoContainer container = (MutablePicoContainer) getProject().getPicoContainer();
+    Class<WorkspacePathResolverProvider> key = WorkspacePathResolverProvider.class;
+    Object oldProvider = container.getComponentInstance(key);
+    container.unregisterComponent(key.getName());
+    container.registerComponentInstance(key.getName(),
+                                        (WorkspacePathResolverProvider) () -> new WorkspacePathResolverImpl(workspaceRoot));
+    if (!settingsInitialized) {
+      Disposer.register(parentDisposable, () -> {
+        container.unregisterComponent(key.getName());
+        if (oldProvider != null) {
+          container.registerComponentInstance(key.getName(), oldProvider);
+        }
+      });
+    }
+  }
+
+  private void setProjectViewText(String projectViewText) {
+    new WriteAction() {
+      @Override
+      protected void run(@NotNull Result result) throws Throwable {
+        projectViewEditor.getDocument().setReadOnly(false);
+        projectViewEditor.getDocument().setText(projectViewText);
+      }
+    }.execute();
+    updateTextAreasEnabled();
+  }
+
+  private void updateTextAreasEnabled() {
+    boolean editEnabled = allowEditShared || !useSharedProjectView;
+    projectViewEditor.setViewer(!editEnabled);
+    projectViewEditor.getDocument().setReadOnly(!editEnabled);
+    projectViewEditor.reinitSettings();
+  }
+
+  public ProjectViewSet parseProjectView(final List<IssueOutput> issues) {
+    final String projectViewText = projectViewEditor.getDocument().getText();
+    final OutputSink<IssueOutput> issueCollector = output -> {
+      issues.add(output);
+      return OutputSink.Propagation.Continue;
+    };
+    return Scope.root(context -> {
+      context.addOutputSink(IssueOutput.class, issueCollector);
+      ProjectViewParser projectViewParser = new ProjectViewParser(context, new WorkspacePathResolverImpl(workspaceRoot));
+      projectViewParser.parseProjectView(projectViewText);
+      return projectViewParser.getResult();
+    });
+  }
+
+  public boolean getUseSharedProjectView() {
+    return this.useSharedProjectView;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncManager.java b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncManager.java
new file mode 100644
index 0000000..c99f9db
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncManager.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync;
+
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.StartupManager;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Manages syncing and its listeners.
+ */
+public class BlazeSyncManager {
+
+  @NotNull
+  private final Project project;
+
+  public BlazeSyncManager(@NotNull Project project) {
+    this.project = project;
+  }
+
+  public static BlazeSyncManager getInstance(@NotNull Project project) {
+    return ServiceManager.getService(project, BlazeSyncManager.class);
+  }
+
+  /**
+   * Requests a project sync with Blaze.
+   */
+  public void requestProjectSync(@NotNull final BlazeSyncParams syncParams) {
+    StartupManager.getInstance(project).runWhenProjectIsInitialized(new Runnable() {
+      @Override
+      public void run() {
+        final BlazeImportSettings importSettings =
+          BlazeImportSettingsManager.getInstance(project).getImportSettings();
+        if (importSettings == null) {
+          throw new IllegalStateException(String.format("Attempt to sync non-%s project.", Blaze.buildSystemName(project)));
+        }
+
+        final BlazeSyncTask syncTask = new BlazeSyncTask(
+          project,
+          importSettings,
+          syncParams
+        );
+
+        BlazeExecutor.submitTask(project, syncTask);
+      }
+    });
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncParams.java b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncParams.java
new file mode 100644
index 0000000..9137fd5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncParams.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+
+import javax.annotation.concurrent.Immutable;
+import java.util.Collection;
+
+/**
+ * Parameters that control the sync.
+ */
+@Immutable
+public final class BlazeSyncParams {
+
+  public enum SyncMode {
+    RESTORE_EPHEMERAL_STATE,
+    INCREMENTAL,
+    FULL
+  }
+
+  public static final class Builder {
+    private String title;
+    private SyncMode syncMode;
+    private boolean backgroundSync = false;
+    private boolean doBuild = true;
+    private ImmutableList.Builder<TargetExpression> targetExpressions = ImmutableList.builder();
+
+    public Builder(String title,
+                   SyncMode syncMode) {
+      this.title = title;
+      this.syncMode = syncMode;
+    }
+
+    public Builder setDoBuild(boolean doBuild) {
+      this.doBuild = doBuild;
+      return this;
+    }
+
+    public Builder setBackgroundSync(boolean backgroundSync) {
+      this.backgroundSync = backgroundSync;
+      return this;
+    }
+
+    public Builder addTargetExpression(TargetExpression targetExpression) {
+      this.targetExpressions.add(targetExpression);
+      return this;
+    }
+
+    public Builder addTargetExpressions(Collection<TargetExpression> targets) {
+      this.targetExpressions.addAll(targets);
+      return this;
+    }
+
+    public BlazeSyncParams build() {
+      return new BlazeSyncParams(title, syncMode, backgroundSync, doBuild, targetExpressions.build());
+    }
+  }
+
+  public final String title;
+  public final SyncMode syncMode;
+  public final boolean backgroundSync;
+  public final boolean doBuild;
+  public final ImmutableList<TargetExpression> targetExpressions;
+
+  private BlazeSyncParams(
+    String title,
+    SyncMode syncMode,
+    boolean backgroundSync,
+    boolean doBuild,
+    ImmutableList<TargetExpression> targetExpressions) {
+    this.title = title;
+    this.syncMode = syncMode;
+    this.backgroundSync = backgroundSync;
+    this.doBuild = doBuild;
+    this.targetExpressions = targetExpressions;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
new file mode 100644
index 0000000..2f65e17
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncPlugin.java
@@ -0,0 +1,290 @@
+/*
+ * 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.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+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.projectview.section.SectionParser;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.openapi.extensions.ExtensionPointName;
+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 javax.annotation.Nullable;
+import java.io.File;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Can plug into the blaze sync system.
+ */
+public interface BlazeSyncPlugin {
+  ExtensionPointName<BlazeSyncPlugin> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.SyncPlugin");
+
+  /**
+   * May be used by the plugin to create/edit modules.
+   *
+   * Using this ensures that the blaze plugin is aware of the modules,
+   * won't garbage collect them, and that all module modifications
+   * happen in a single transaction.
+   */
+  interface ModuleEditor {
+    /**
+     * Creates a new module and registers it with the module editor.
+     */
+    Module createModule(String moduleName, ModuleType moduleType);
+
+    /**
+     * Edits a module. It will be committed when commit is called.
+     *
+     * <p>The module will be returned in a cleared state. You should
+     * not call this method multiple times.
+     */
+    ModifiableRootModel editModule(Module module);
+
+    /**
+     * Registers a module. This prevents garbage collection of
+     * the module upon commit.
+     *
+     * @return True if the module exists and was registered.
+     */
+    boolean registerModule(String moduleName);
+
+    /**
+     * Finds a module by name. This doesn't register the module.
+     */
+    @Nullable
+    Module findModule(String moduleName);
+
+    /**
+     * Commits the module editor without garbage collection.
+     */
+    void commit();
+  }
+
+  /**
+   * @return The default workspace type recommended by this plugin.
+   */
+  @Nullable
+  WorkspaceType getDefaultWorkspaceType();
+
+  /**
+   * @return The module type for the workspace given the workspace type.
+   */
+  @Nullable
+  ModuleType getWorkspaceModuleType(WorkspaceType workspaceType);
+
+  /**
+   * @return The set of supported languages under this workspace type.
+   */
+  Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType);
+
+  void updateSyncState(
+    Project project,
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet,
+    WorkspaceLanguageSettings workspaceLanguageSettings,
+    BlazeRoots blazeRoots,
+    @Nullable WorkingSet workingSet,
+    WorkspacePathResolver workspacePathResolver,
+    ImmutableMap<Label, RuleIdeInfo> ruleMap,
+    @Deprecated @Nullable File androidPlatformDirectory,
+    SyncState.Builder syncStateBuilder,
+    @Nullable SyncState previousSyncState
+  );
+
+  /**
+   * Updates the sdk.
+   */
+  void updateSdk(Project project,
+                 BlazeContext context,
+                 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);
+
+  void updateProjectStructure(
+    Project project,
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet,
+    BlazeProjectData blazeProjectData,
+    @Nullable BlazeProjectData oldBlazeProjectData,
+    ModuleEditor moduleEditor,
+    Module workspaceModule,
+    ModifiableRootModel workspaceModifiableModel
+  );
+
+  /**
+   * Validates the project.
+   */
+  boolean validate(Project project,
+                   BlazeContext context,
+                   BlazeProjectData blazeProjectData);
+
+  /**
+   * Validates the project view.
+   * @return True for success, false for fatal error.
+   */
+  boolean validateProjectView(
+    BlazeContext context,
+    ProjectViewSet projectViewSet,
+    WorkspaceLanguageSettings workspaceLanguageSettings
+  );
+
+  /**
+   * Returns any custom sections that this plugin supports.
+   */
+  Collection<SectionParser> getSections();
+
+  /**
+   * Returns whether this plugin requires resolving ide artifacts to function.
+   */
+  boolean requiresResolveIdeArtifacts();
+
+  /**
+   * Whether this plugin requires an Android SDK to function.
+   */
+  boolean requiresAndroidSdk(WorkspaceLanguageSettings workspaceLanguageSettings);
+
+  /**
+   * Returns any source file extensions that are a good candidate for the {@link com.google.idea.blaze.base.prefetch.Prefetcher}.
+   */
+  Set<String> prefetchSrcFileExtensions();
+
+  class Adapter implements BlazeSyncPlugin {
+
+    @Nullable
+    @Override
+    public WorkspaceType getDefaultWorkspaceType() {
+      return null;
+    }
+
+    @Nullable
+    @Override
+    public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
+      return null;
+    }
+
+    @Override
+    public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+      return ImmutableSet.of();
+    }
+
+    @Override
+    public void updateSyncState(Project project,
+                                BlazeContext context,
+                                WorkspaceRoot workspaceRoot,
+                                ProjectViewSet projectViewSet,
+                                WorkspaceLanguageSettings workspaceLanguageSettings,
+                                BlazeRoots blazeRoots,
+                                @Nullable WorkingSet workingSet,
+                                WorkspacePathResolver workspacePathResolver,
+                                ImmutableMap<Label, RuleIdeInfo> ruleMap,
+                                @Deprecated @Nullable File androidPlatformDirectory,
+                                SyncState.Builder syncStateBuilder,
+                                @Nullable SyncState previousSyncState) {
+    }
+
+    @Override
+    public void updateSdk(Project project,
+                          BlazeContext context,
+                          ProjectViewSet projectViewSet,
+                          BlazeProjectData blazeProjectData) {
+    }
+
+    @Override
+    public void updateContentEntries(Project project,
+                                     BlazeContext context,
+                                     WorkspaceRoot workspaceRoot,
+                                     ProjectViewSet projectViewSet,
+                                     BlazeProjectData blazeProjectData,
+                                     Collection<ContentEntry> contentEntries) {
+    }
+
+    @Override
+    public void updateProjectStructure(Project project,
+                                       BlazeContext context,
+                                       WorkspaceRoot workspaceRoot,
+                                       ProjectViewSet projectViewSet,
+                                       BlazeProjectData blazeProjectData,
+                                       @Nullable BlazeProjectData oldBlazeProjectData,
+                                       ModuleEditor moduleEditor,
+                                       Module workspaceModule,
+                                       ModifiableRootModel workspaceModifiableModel) {
+    }
+
+    @Override
+    public boolean validate(Project project,
+                            BlazeContext context,
+                            BlazeProjectData blazeProjectData) {
+      return true;
+    }
+
+    @Override
+    public boolean validateProjectView(BlazeContext context,
+                                       ProjectViewSet projectViewSet,
+                                       WorkspaceLanguageSettings workspaceLanguageSettings) {
+      return true;
+    }
+
+    @Override
+    public Collection<SectionParser> getSections() {
+      return ImmutableList.of();
+    }
+
+    @Override
+    public boolean requiresResolveIdeArtifacts() {
+      return false;
+    }
+
+    @Override
+    public boolean requiresAndroidSdk(WorkspaceLanguageSettings workspaceLanguageSettings) {
+      return false;
+    }
+
+    @Override
+    public Set<String> prefetchSrcFileExtensions() {
+      return ImmutableSet.of();
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncStartupActivity.java b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncStartupActivity.java
new file mode 100644
index 0000000..726a5ae
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncStartupActivity.java
@@ -0,0 +1,50 @@
+/*
+ * 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.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.actions.IncrementalSyncProjectAction;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatusImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.startup.StartupActivity;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Syncs the project upon startup.
+ */
+public class BlazeSyncStartupActivity implements StartupActivity {
+
+  @Override
+  public void runActivity(@NotNull final Project project) {
+    BlazeImportSettings importSettings = BlazeImportSettingsManager.getInstance(project)
+      .getImportSettings();
+
+    if (importSettings != null) {
+      BlazeSyncManager.getInstance(project).requestProjectSync(getSyncParams());
+    }
+  }
+
+  private static BlazeSyncParams getSyncParams() {
+    if (BlazeSyncStatusImpl.AUTOMATIC_INCREMENTAL_SYNC.getValue()) {
+      return IncrementalSyncProjectAction.startupSyncParams;
+    }
+    return new BlazeSyncParams.Builder(
+      "Sync Project",
+      BlazeSyncParams.SyncMode.RESTORE_EPHEMERAL_STATE)
+      .build();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
new file mode 100755
index 0000000..788906c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/BlazeSyncTask.java
@@ -0,0 +1,679 @@
+/*
+ * 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.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.idea.blaze.base.async.AsyncUtil;
+import com.google.idea.blaze.base.async.FutureUtil;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.experiments.ExperimentScope;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewVerifier;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.rulemaps.ReverseDependencyMap;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.google.idea.blaze.base.scope.scopes.*;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin.ModuleEditor;
+import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManagerImpl;
+import com.google.idea.blaze.base.sync.projectstructure.ContentEntryEditor;
+import com.google.idea.blaze.base.sync.projectstructure.ModuleDataStorage;
+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.LanguageSupport;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.*;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.google.idea.blaze.base.vcs.BlazeVcsHandler;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleType;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.progress.Progressive;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ContentEntry;
+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.LocalFileSystem;
+import com.intellij.openapi.vfs.StandardFileSystems;
+import com.intellij.openapi.vfs.VirtualFileManager;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * Syncs the project with blaze.
+ */
+final class BlazeSyncTask implements Progressive {
+
+  private final static Logger LOG = Logger.getInstance(BlazeSyncTask.class);
+
+  private final Project project;
+  private final BlazeImportSettings importSettings;
+  private final WorkspaceRoot workspaceRoot;
+  private final BlazeSyncParams syncParams;
+  private final boolean expandSyncToWorkingSet;
+  private final boolean showPerformanceWarnings;
+  private long syncStartTime;
+
+  BlazeSyncTask(
+    Project project,
+    BlazeImportSettings importSettings,
+    final BlazeSyncParams syncParams) {
+    this.project = project;
+    this.importSettings = importSettings;
+    this.workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+    this.syncParams = syncParams;
+    this.expandSyncToWorkingSet = BlazeUserSettings.getInstance().getExpandSyncToWorkingSet()
+                                  && ExpandWorkingSetTargetsExperiment.ENABLE_EXPAND_WORKING_SET_TARGETS.getValue();
+    this.showPerformanceWarnings = BlazeUserSettings.getInstance().getShowPerformanceWarnings();
+  }
+
+
+  @Override
+  public void run(final ProgressIndicator indicator) {
+    Scope.root((BlazeContext context) -> {
+      context.push(new ExperimentScope());
+      if (showPerformanceWarnings) {
+        context.push(new PerformanceWarningScope());
+      }
+      context
+        .push(new ProgressIndicatorScope(indicator))
+        .push(new TimingScope("Sync"))
+        .push(new LoggedTimingScope(project, Action.SYNC_TOTAL_TIME))
+      ;
+
+      if (!syncParams.backgroundSync) {
+        context
+          .push(new BlazeConsoleScope.Builder(project, indicator).build())
+          .push(new IssuesScope(project))
+          .push(new NotificationScope(
+            project,
+            "Sync",
+            "Sync project",
+            "Sync successful",
+            "Sync failed"
+          ))
+        ;
+      }
+
+      context.output(new StatusOutput("Syncing project..."));
+      syncProject(context);
+    });
+  }
+
+  /**
+   * Returns true if sync successfully completed
+   */
+  @VisibleForTesting
+  boolean syncProject(BlazeContext context) {
+    boolean success = false;
+    try {
+      SaveUtil.saveAllFiles();
+      onSyncStart(project);
+      success = doSyncProject(context);
+    } catch (AssertionError|Exception e) {
+      LOG.error(e);
+      IssueOutput.error("Internal error: " + e.getMessage()).submit(context);
+    } finally {
+      afterSync(project, success);
+    }
+    return success;
+  }
+
+  /**
+   * @return true if sync successfully completed
+   */
+  private boolean doSyncProject(final BlazeContext context) {
+    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 false;
+    }
+
+    @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(project, workspaceRoot)) {
+        vcsHandler = candidate;
+        break;
+      }
+    }
+    if (vcsHandler == null) {
+      IssueOutput.error("Could not find a VCS handler").submit(context);
+      return false;
+    }
+
+    ListeningExecutorService executor = BlazeExecutor.getInstance().getExecutor();
+    ListenableFuture<BlazeRoots> blazeRootsFuture = BlazeRoots.compute(project, workspaceRoot, context);
+    ListenableFuture<WorkingSet> workingSetFuture = vcsHandler.getWorkingSet(project, workspaceRoot, executor);
+
+    BlazeRoots blazeRoots = FutureUtil.waitForFuture(context, blazeRootsFuture)
+      .timed(Blaze.buildSystemName(project) + "Roots")
+      .withProgressMessage(String.format("Running %s info...", Blaze.buildSystemName(project)))
+      .onError(String.format("Could not get %s roots", Blaze.buildSystemName(project)))
+      .run()
+      .result();
+    if (blazeRoots == null) {
+      return false;
+    }
+
+    WorkspacePathResolverAndProjectView workspacePathResolverAndProjectView = computeWorkspacePathResolverAndProjectView(
+      context,
+      blazeRoots,
+      vcsHandler,
+      executor
+    );
+    if (workspacePathResolverAndProjectView == null) {
+      return false;
+    }
+    WorkspacePathResolver workspacePathResolver = workspacePathResolverAndProjectView.workspacePathResolver;
+    ProjectViewSet projectViewSet = workspacePathResolverAndProjectView.projectViewSet;
+
+    WorkspaceLanguageSettings workspaceLanguageSettings = LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    if (workspaceLanguageSettings == null) {
+      return false;
+    }
+
+    if (!ProjectViewVerifier.verifyProjectView(context, workspaceRoot, projectViewSet, workspaceLanguageSettings)) {
+      return false;
+    }
+
+    final BlazeProjectData newBlazeProjectData;
+
+    WorkingSet workingSet = FutureUtil.waitForFuture(context, workingSetFuture)
+      .timed("WorkingSet")
+      .withProgressMessage("Computing VCS working set...")
+      .onError("Could not compute working set")
+      .run()
+      .result();
+    if (!context.shouldContinue()) {
+      return false;
+    }
+
+    if (workingSet != null) {
+      printWorkingSet(context, workingSet);
+    }
+
+    boolean ideResolveErrors = false;
+    if (syncParams.syncMode != SyncMode.RESTORE_EPHEMERAL_STATE || oldBlazeProjectData == null) {
+      SyncState.Builder syncStateBuilder = new SyncState.Builder();
+      SyncState previousSyncState = oldBlazeProjectData != null ? oldBlazeProjectData.syncState : null;
+      List<TargetExpression> allTargets = projectViewSet.listItems(TargetSection.KEY);
+      if (expandSyncToWorkingSet && workingSet != null) {
+        allTargets.addAll(getWorkingSetTargets(workingSet));
+      }
+
+      boolean syncPluginRequiresBuild = false;
+      boolean requiresAndroidSdk = false;
+      for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+        syncPluginRequiresBuild |= syncPlugin.requiresResolveIdeArtifacts();
+        requiresAndroidSdk |= syncPlugin.requiresAndroidSdk(workspaceLanguageSettings);
+      }
+
+      final BlazeIdeInterface.IdeResult ideQueryResult = getIdeQueryResult(
+        project,
+        context,
+        projectViewSet,
+        allTargets,
+        workspaceLanguageSettings,
+        new ArtifactLocationDecoder(blazeRoots, workspacePathResolver),
+        syncStateBuilder,
+        previousSyncState,
+        requiresAndroidSdk
+      );
+      if (ideQueryResult == null) {
+        if (workingSet != null && !workingSet.isEmpty() && expandSyncToWorkingSet && !context.isCancelled()) {
+          String msg = String.format("If you have broken targets in your VCS working set, uncheck '%s > Expand Sync to Working Set'" +
+                                     " and try again.", Blaze.buildSystemName(project));
+          context.output(new PrintOutput(msg, PrintOutput.OutputType.ERROR));
+        }
+        return false;
+      }
+      ImmutableMap<Label, RuleIdeInfo> ruleMap = ideQueryResult.ruleMap;
+
+      ListenableFuture<ImmutableMultimap<Label, Label>> reverseDependenciesFuture =
+        BlazeExecutor.getInstance().submit(() -> ReverseDependencyMap.createRdepsMap(ruleMap));
+
+      boolean doBuild = syncPluginRequiresBuild || (syncParams.doBuild || oldBlazeProjectData == null);
+      if (doBuild) {
+        List<TargetExpression> targetExpressions = Lists.newArrayList(syncParams.targetExpressions);
+        if (targetExpressions.isEmpty()) {
+          targetExpressions.addAll(allTargets);
+        }
+        ideResolveErrors = !resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, targetExpressions);
+        LocalFileSystem.getInstance().refresh(true);
+        if (context.isCancelled()) {
+          return false;
+        }
+      }
+
+      Scope.push(context, (childContext) -> {
+        childContext.push(new TimingScope("UpdateSyncState"));
+        for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+          syncPlugin.updateSyncState(
+            project,
+            childContext,
+            workspaceRoot,
+            projectViewSet,
+            workspaceLanguageSettings,
+            blazeRoots,
+            workingSet,
+            workspacePathResolver,
+            ruleMap,
+            ideQueryResult.androidPlatformDirectory,
+            syncStateBuilder,
+            previousSyncState);
+        }
+      });
+
+      ImmutableMultimap<Label, Label> reverseDependencies = FutureUtil.waitForFuture(context, reverseDependenciesFuture)
+        .timed("ReverseDependencies")
+        .onError("Failed to compute reverse dependency map")
+        .run()
+        .result();
+      if (reverseDependencies == null) {
+        return false;
+      }
+
+      newBlazeProjectData = new BlazeProjectData(
+        syncStartTime,
+        ruleMap,
+        blazeRoots,
+        workingSet,
+        workspacePathResolver,
+        workspaceLanguageSettings,
+        syncStateBuilder.build(),
+        reverseDependencies
+      );
+    } else {
+      // Restore project based on old blaze project data
+      newBlazeProjectData = oldBlazeProjectData;
+    }
+
+    boolean success = updateProject(project, context, projectViewSet, oldBlazeProjectData, newBlazeProjectData);
+    if (!success) {
+      return false;
+    }
+
+    if (ideResolveErrors) {
+      context.output(new PrintOutput(
+        "Sync was successful, but there were compilation errors. The project may not fully resolve until fixed.",
+        PrintOutput.OutputType.ERROR
+      ));
+    }
+
+    onSyncComplete(project, context, projectViewSet, newBlazeProjectData);
+    return true;
+  }
+
+  static class WorkspacePathResolverAndProjectView {
+    final WorkspacePathResolver workspacePathResolver;
+    final ProjectViewSet projectViewSet;
+    public WorkspacePathResolverAndProjectView(WorkspacePathResolver workspacePathResolver,
+                                               ProjectViewSet projectViewSet) {
+      this.workspacePathResolver = workspacePathResolver;
+      this.projectViewSet = projectViewSet;
+    }
+  }
+  private WorkspacePathResolverAndProjectView computeWorkspacePathResolverAndProjectView(BlazeContext context,
+                                                                                         BlazeRoots blazeRoots,
+                                                                                         BlazeVcsHandler vcsHandler,
+                                                                                         ListeningExecutorService executor) {
+    for (int i = 0; i < 3; ++i) {
+      WorkspacePathResolver vcsWorkspacePathResolver = null;
+      BlazeVcsHandler.BlazeVcsSyncHandler vcsSyncHandler = vcsHandler.createSyncHandler(project, workspaceRoot);
+      if (vcsSyncHandler != null) {
+        boolean ok = Scope.push(context, (childContext) -> {
+          childContext.push(new TimingScope("UpdateVcs"));
+          return vcsSyncHandler.update(context, blazeRoots, executor);
+        });
+        if (!ok) {
+          return null;
+        }
+        vcsWorkspacePathResolver = vcsSyncHandler.getWorkspacePathResolver();
+      }
+
+      WorkspacePathResolver workspacePathResolver = vcsWorkspacePathResolver != null
+                                                    ? vcsWorkspacePathResolver
+                                                    : new WorkspacePathResolverImpl(workspaceRoot, blazeRoots);
+
+      ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).reloadProjectView(context, workspacePathResolver);
+      if (projectViewSet == null) {
+        return null;
+      }
+
+      if (vcsSyncHandler != null) {
+        BlazeVcsHandler.BlazeVcsSyncHandler.ValidationResult validationResult =
+          vcsSyncHandler.validateProjectView(context, projectViewSet);
+        switch (validationResult) {
+          case OK:
+            // Fall-through and return
+            break;
+          case Error:
+            return null;
+          case RestartSync:
+            continue;
+          default:
+            // Cannot happen
+            return null;
+        }
+      }
+
+      return new WorkspacePathResolverAndProjectView(workspacePathResolver, projectViewSet);
+    }
+    return null;
+  }
+
+  private void printWorkingSet(BlazeContext context, WorkingSet workingSet) {
+    List<String> messages = Lists.newArrayList();
+    messages.addAll(workingSet.addedFiles.stream().map(file -> file.relativePath() + " (added)").collect(Collectors.toList()));
+    messages.addAll(workingSet.modifiedFiles.stream().map(file -> file.relativePath() + " (modified)").collect(Collectors.toList()));
+    Collections.sort(messages);
+
+    if (messages.isEmpty()) {
+      context.output(new PrintOutput("Your working set is empty"));
+      return;
+    }
+    int maxFiles = 20;
+    for (String message : Iterables.limit(messages, maxFiles)) {
+      context.output(new PrintOutput("  " + message));
+    }
+    if (messages.size() > maxFiles) {
+      context.output(new PrintOutput(String.format("  (and %d more)", messages.size() - maxFiles)));
+    }
+  }
+
+  private Collection<? extends TargetExpression> getWorkingSetTargets(WorkingSet workingSet) {
+    List<TargetExpression> result = Lists.newArrayList();
+    for (WorkspacePath workspacePath : Iterables.concat(workingSet.addedFiles, workingSet.modifiedFiles)) {
+      File buildFile = workspaceRoot.fileForPath(workspacePath);
+      if (buildFile.getName().equals("BUILD")) {
+        result.add(TargetExpression.allFromPackageNonRecursive(workspaceRoot.workspacePathFor(buildFile.getParentFile())));
+      }
+    }
+    return result;
+  }
+
+
+  private boolean updateProject(Project project,
+                                BlazeContext parentContext,
+                                ProjectViewSet projectViewSet,
+                                @Nullable BlazeProjectData oldBlazeProjectData,
+                                BlazeProjectData newBlazeProjectData) {
+    return Scope.push(parentContext, context -> {
+      context
+        .push(new LoggedTimingScope(project, Action.SYNC_IMPORT_DATA_TIME))
+        .push(new TimingScope("UpdateProjectStructure"));
+      context.output(new StatusOutput("Committing project structure..."));
+
+      return updateProject(
+        context,
+        importSettings,
+        projectViewSet,
+        oldBlazeProjectData,
+        newBlazeProjectData
+      );
+    });
+  }
+
+  @Nullable
+  private BlazeIdeInterface.IdeResult getIdeQueryResult(
+    Project project,
+    BlazeContext parentContext,
+    ProjectViewSet projectViewSet,
+    List<TargetExpression> targets,
+    WorkspaceLanguageSettings workspaceLanguageSettings,
+    ArtifactLocationDecoder artifactLocationDecoder,
+    SyncState.Builder syncStateBuilder,
+    @Nullable SyncState previousSyncState,
+    boolean requiresAndroidSdk) {
+
+    return Scope.push(parentContext, context -> {
+      context.push(new TimingScope("IdeQuery"));
+
+      BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
+      return blazeIdeInterface.updateBlazeIdeState(
+        project,
+        context,
+        workspaceRoot,
+        projectViewSet,
+        targets,
+        workspaceLanguageSettings,
+        artifactLocationDecoder,
+        syncStateBuilder,
+        previousSyncState,
+        requiresAndroidSdk
+      );
+    });
+  }
+
+  private static boolean resolveIdeArtifacts(
+    Project project,
+    BlazeContext parentContext,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet,
+    List<TargetExpression> targetExpressions) {
+    return Scope.push(parentContext, context -> {
+      context
+        .push(new LoggedTimingScope(project, Action.BLAZE_BUILD_DURING_SYNC))
+        .push(new TimingScope("BlazeBuild"))
+      ;
+      context.output(new StatusOutput("Building project dependencies..."));
+
+      // We don't want errors propagated for the build step - compilation errors shouldn't be interpreted
+      // as "Sync failed"
+      context.setPropagatesErrors(false);
+
+      if (!targetExpressions.isEmpty()) {
+        BlazeIdeInterface blazeIdeInterface = BlazeIdeInterface.getInstance();
+        blazeIdeInterface.resolveIdeArtifacts(project, context, workspaceRoot, projectViewSet, targetExpressions);
+      }
+
+      return !context.hasErrors();
+    });
+  }
+
+  private boolean updateProject(
+    BlazeContext context,
+    BlazeImportSettings importSettings,
+    ProjectViewSet projectViewSet,
+    @Nullable BlazeProjectData oldBlazeProjectData,
+    BlazeProjectData newBlazeProjectData) {
+
+    try {
+      AsyncUtil.executeProjectChangeAction(() -> ProjectRootManagerEx.getInstanceEx(project).mergeRootsChangesDuring(() -> {
+        updateSdk(
+          context,
+          projectViewSet,
+          newBlazeProjectData
+        );
+        updateProjectStructure(
+          context,
+          importSettings,
+          projectViewSet,
+          oldBlazeProjectData,
+          newBlazeProjectData
+        );
+      }));
+    } catch (Throwable t) {
+      IssueOutput.error("Internal error. Please issue a bug at go/aswbbug. Error: " + t)
+        .submit(context);
+      LOG.error(t);
+      return false;
+    }
+
+    BlazeProjectDataManagerImpl.getImpl(project).saveProject(importSettings, newBlazeProjectData);
+    return true;
+  }
+
+  private void updateSdk(BlazeContext context,
+                         ProjectViewSet projectViewSet,
+                         BlazeProjectData newBlazeProjectData) {
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      syncPlugin.updateSdk(project, context, projectViewSet, newBlazeProjectData);
+    }
+  }
+
+  private void updateProjectStructure(
+    BlazeContext context,
+    BlazeImportSettings importSettings,
+    ProjectViewSet projectViewSet,
+    @Nullable BlazeProjectData oldBlazeProjectData,
+    BlazeProjectData newBlazeProjectData) {
+
+    ModuleEditorImpl moduleEditor = ModuleEditorProvider.getInstance().getModuleEditor(project, importSettings);
+
+    ModuleType workspaceModuleType = null;
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      workspaceModuleType = syncPlugin.getWorkspaceModuleType(newBlazeProjectData.workspaceLanguageSettings.getWorkspaceType());
+      if (workspaceModuleType != null) {
+        break;
+      }
+    }
+    if (workspaceModuleType == null) {
+      workspaceModuleType = ModuleType.EMPTY;
+      IssueOutput.warn("Could not set module type for workspace module.").submit(context);
+    }
+
+    Module workspaceModule = moduleEditor.createModule(ModuleDataStorage.WORKSPACE_MODULE_NAME, workspaceModuleType);
+    ModifiableRootModel workspaceModifiableModel = moduleEditor.editModule(workspaceModule);
+
+    ContentEntryEditor.createContentEntries(
+      project,
+      context,
+      workspaceRoot,
+      projectViewSet,
+      newBlazeProjectData,
+      workspaceModifiableModel
+    );
+
+    for (BlazeSyncPlugin blazeSyncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      blazeSyncPlugin.updateProjectStructure(
+        project,
+        context,
+        workspaceRoot,
+        projectViewSet,
+        newBlazeProjectData,
+        oldBlazeProjectData,
+        moduleEditor,
+        workspaceModule,
+        workspaceModifiableModel);
+    }
+
+    createProjectDataDirectoryModule(moduleEditor, new File(importSettings.getProjectDataDirectory()), workspaceModuleType);
+
+    moduleEditor.commitWithGc(context);
+  }
+
+  /**
+   * Creates a module that includes the user's data directory.
+   *
+   * This is useful to be able to edit the project view without IntelliJ complaining it's outside the project.
+   */
+  private void createProjectDataDirectoryModule(ModuleEditor moduleEditor,
+                                                File projectDataDirectory,
+                                                ModuleType moduleType) {
+    Module module = moduleEditor.createModule(ModuleDataStorage.PROJECT_DATA_DIR_MODULE_NAME, moduleType);
+    ModifiableRootModel modifiableModel = moduleEditor.editModule(module);
+    ContentEntry rootContentEntry = modifiableModel.addContentEntry(pathToUrl(projectDataDirectory));
+    rootContentEntry.addExcludeFolder(pathToUrl(new File(projectDataDirectory, ".idea")));
+    rootContentEntry.addExcludeFolder(pathToUrl(new File(projectDataDirectory, ModuleDataStorage.DATA_SUBDIRECTORY)));
+  }
+
+  private static String pathToUrl(File path) {
+    String filePath = FileUtil.toSystemIndependentName(path.getPath());
+    return VirtualFileManager.constructUrl(StandardFileSystems.FILE_PROTOCOL, filePath);
+  }
+
+  private static void onSyncStart(Project project) {
+    final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
+    for (SyncListener syncListener : syncListeners) {
+      syncListener.onSyncStart(project);
+    }
+  }
+
+  private static void afterSync(Project project,
+                                boolean successful) {
+    final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
+    for (SyncListener syncListener : syncListeners) {
+      syncListener.afterSync(project, successful);
+    }
+  }
+
+  private void onSyncComplete(Project project,
+                              BlazeContext context,
+                              ProjectViewSet projectViewSet,
+                              BlazeProjectData blazeProjectData) {
+    validate(project, context, blazeProjectData);
+
+    final SyncListener[] syncListeners = SyncListener.EP_NAME.getExtensions();
+    for (SyncListener syncListener : syncListeners) {
+      syncListener.onSyncComplete(
+        project,
+        importSettings,
+        projectViewSet,
+        blazeProjectData
+      );
+    }
+  }
+
+  private static void validate(
+    Project project,
+    BlazeContext context,
+    BlazeProjectData blazeProjectData) {
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      syncPlugin.validate(project, context, blazeProjectData);
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/ExpandWorkingSetTargetsExperiment.java b/blaze-base/src/com/google/idea/blaze/base/sync/ExpandWorkingSetTargetsExperiment.java
new file mode 100644
index 0000000..29e189e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/ExpandWorkingSetTargetsExperiment.java
@@ -0,0 +1,25 @@
+/*
+ * 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.idea.blaze.base.experiments.BoolExperiment;
+
+/**
+ * Experiment holder for the working set experiment.
+ */
+public class ExpandWorkingSetTargetsExperiment {
+  public static final BoolExperiment ENABLE_EXPAND_WORKING_SET_TARGETS = new BoolExperiment("enable.expand.working.set.targets", true);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/SyncListener.java b/blaze-base/src/com/google/idea/blaze/base/sync/SyncListener.java
new file mode 100644
index 0000000..4e7fd71
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/SyncListener.java
@@ -0,0 +1,72 @@
+/*
+ * 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.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Extension interface for listening to syncs.
+ */
+public interface SyncListener {
+  ExtensionPointName<SyncListener> EP_NAME = ExtensionPointName
+    .create("com.google.idea.blaze.SyncListener");
+
+  /**
+   * Called after open documents have been saved, prior to starting the blaze sync.
+   */
+  void onSyncStart(Project project);
+
+  /**
+   * Called on successful completion of a sync
+   */
+  void onSyncComplete(
+    Project project,
+    BlazeImportSettings importSettings,
+    ProjectViewSet projectViewSet,
+    BlazeProjectData blazeProjectData);
+
+  /**
+   * Guaranteed to be called once per sync,
+   * regardless of whether it successfully completed
+   */
+  void afterSync(Project project,
+                 boolean successful);
+
+  /**
+   * Convenience adapter class.
+   */
+  abstract class Adapter implements SyncListener {
+
+    @Override
+    public void onSyncStart(Project project) {
+    }
+
+    @Override
+    public void onSyncComplete(Project project,
+                               BlazeImportSettings importSettings,
+                               ProjectViewSet projectViewSet,
+                               BlazeProjectData blazeProjectData) {
+    }
+
+    @Override
+    public void afterSync(Project project, boolean successful) {
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/actions/ExpandSyncToWorkingSetAction.java b/blaze-base/src/com/google/idea/blaze/base/sync/actions/ExpandSyncToWorkingSetAction.java
new file mode 100644
index 0000000..5b577a9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/actions/ExpandSyncToWorkingSetAction.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.actions;
+
+import com.google.idea.blaze.base.actions.BlazeToggleAction;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.ExpandWorkingSetTargetsExperiment;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Manages a tick box of whether to expand the sync targets to the working set.
+ */
+public class ExpandSyncToWorkingSetAction extends BlazeToggleAction {
+  @Override
+  public boolean isSelected(AnActionEvent e) {
+    return BlazeUserSettings.getInstance().getExpandSyncToWorkingSet();
+  }
+
+  @Override
+  public void setSelected(AnActionEvent e, boolean state) {
+    BlazeUserSettings.getInstance().setExpandSyncToWorkingSet(state);
+  }
+
+  @Override
+  protected void doUpdate(@NotNull AnActionEvent e) {
+    super.doUpdate(e);
+
+    boolean enabled = ExpandWorkingSetTargetsExperiment.ENABLE_EXPAND_WORKING_SET_TARGETS.getValue();
+    e.getPresentation().setVisible(enabled);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/actions/FullSyncProjectAction.java b/blaze-base/src/com/google/idea/blaze/base/sync/actions/FullSyncProjectAction.java
new file mode 100644
index 0000000..853181b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/actions/FullSyncProjectAction.java
@@ -0,0 +1,53 @@
+/*
+ * 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.actions;
+
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Re-imports (syncs) an Android-Blaze project, without showing the "Import Project" wizard.
+ */
+public class FullSyncProjectAction extends BlazeAction {
+
+  public FullSyncProjectAction() {
+    super("Non-Incrementally Sync Project with BUILD Files");
+  }
+
+  @Override
+  public void actionPerformed(final AnActionEvent e) {
+    Project project = e.getProject();
+    if (project != null) {
+      Presentation presentation = e.getPresentation();
+      presentation.setEnabled(false);
+      try {
+        BlazeSyncParams syncParams = new BlazeSyncParams.Builder(
+          "Full Sync",
+          SyncMode.FULL
+        ).build();
+        BlazeSyncManager.getInstance(project).requestProjectSync(syncParams);
+      }
+      finally {
+        presentation.setEnabled(true);
+      }
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/actions/IncrementalSyncProjectAction.java b/blaze-base/src/com/google/idea/blaze/base/sync/actions/IncrementalSyncProjectAction.java
new file mode 100644
index 0000000..c82265f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/actions/IncrementalSyncProjectAction.java
@@ -0,0 +1,120 @@
+/*
+ * 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.actions;
+
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.BlazeSyncParams.SyncMode;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatus;
+import com.google.idea.blaze.base.sync.status.BlazeSyncStatusImpl;
+import com.intellij.notification.*;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.project.Project;
+import icons.BlazeIcons;
+
+import javax.swing.*;
+
+
+/**
+ * Re-imports (syncs) an Android-Blaze project, without showing the "Import Project" wizard.
+ */
+public class IncrementalSyncProjectAction extends BlazeAction {
+
+  public static final String ACTION_ID = "Blaze.IncrementalSyncProject";
+
+  public static final BlazeSyncParams manualSyncParams =
+    new BlazeSyncParams.Builder("Sync", SyncMode.INCREMENTAL).build();
+
+  public static final BlazeSyncParams autoSyncParams =
+    new BlazeSyncParams.Builder("Sync", SyncMode.INCREMENTAL)
+    .setBackgroundSync(true)
+    .setDoBuild(false)
+    .build();
+
+  public static final BlazeSyncParams startupSyncParams =
+    new BlazeSyncParams.Builder("Sync", SyncMode.INCREMENTAL)
+      .setDoBuild(false)
+      .build();
+
+  public IncrementalSyncProjectAction() {
+    super("Sync Project with BUILD Files");
+  }
+
+  @Override
+  public void actionPerformed(final AnActionEvent e) {
+    Project project = e.getProject();
+    if (project != null) {
+      BlazeSyncManager.getInstance(project).requestProjectSync(manualSyncParams);
+      updateIcon(e);
+    }
+  }
+
+  @Override
+  protected void doUpdate(AnActionEvent e) {
+    super.doUpdate(e);
+    updateIcon(e);
+  }
+
+  private static void updateIcon(AnActionEvent e) {
+    Project project = e.getProject();
+    Presentation presentation = e.getPresentation();
+    if (project == null) {
+      presentation.setIcon(BlazeIcons.Blaze);
+      presentation.setEnabled(true);
+      return;
+    }
+    BlazeSyncStatusImpl statusHelper = BlazeSyncStatusImpl.getImpl(project);
+    BlazeSyncStatus.SyncStatus status = statusHelper.getStatus();
+    presentation.setIcon(getIcon(status));
+    presentation.setEnabled(!statusHelper.syncInProgress.get());
+
+    if (status == BlazeSyncStatus.SyncStatus.DIRTY && !BlazeUserSettings.getInstance().getSyncStatusPopupShown()) {
+      BlazeUserSettings.getInstance().setSyncStatusPopupShown(true);
+      showPopupNotification(project);
+    }
+  }
+
+  private static Icon getIcon(BlazeSyncStatus.SyncStatus status) {
+    switch (status) {
+      case FAILED: return BlazeIcons.BlazeFailed;
+      case DIRTY: return BlazeIcons.BlazeDirty;
+      case CLEAN: return BlazeIcons.BlazeClean;
+      default: return BlazeIcons.Blaze;
+    }
+  }
+
+  private static final NotificationGroup NOTIFICATION_GROUP =
+    new NotificationGroup("Changes since last blaze sync", NotificationDisplayType.BALLOON, true);
+
+  private static void showPopupNotification(Project project) {
+    String msg = "Some relevant files (e.g. BUILD files, .blazeproject file) have changed " +
+                 "since the last sync. Please press the 'Sync' button in the toolbar to " +
+                 "re-sync your IntelliJ project.";
+    Notification notification = new Notification(
+      NOTIFICATION_GROUP.getDisplayId(),
+      String.format("Changes since last %s sync", Blaze.buildSystemName(project)),
+      msg,
+      NotificationType.INFORMATION);
+    notification.setImportant(true);
+    Notifications.Bus.notify(notification, project);
+  }
+
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java b/blaze-base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
new file mode 100644
index 0000000..43eeab6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/actions/PartialSyncAction.java
@@ -0,0 +1,114 @@
+/*
+ * 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.actions;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.List;
+
+/**
+ * Allows a partial sync of the project depending on what's been selected.
+ */
+public class PartialSyncAction extends BlazeAction {
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    if (project != null) {
+      List<TargetExpression> targetExpressions = Lists.newArrayList();
+      getTargets(e, targetExpressions);
+
+      BlazeSyncParams syncParams = new BlazeSyncParams.Builder("Partial Sync", BlazeSyncParams.SyncMode.INCREMENTAL)
+        .setDoBuild(true)
+        .setBackgroundSync(false)
+        .addTargetExpressions(targetExpressions)
+        .build();
+
+      BlazeSyncManager.getInstance(project).requestProjectSync(syncParams);
+    }
+  }
+
+  @Override
+  protected void doUpdate(AnActionEvent e) {
+    super.doUpdate(e);
+    List<TargetExpression> targets = Lists.newArrayList();
+    String objectName = getTargets(e, targets);
+
+    boolean enabled = objectName != null && !targets.isEmpty();
+    Presentation presentation = e.getPresentation();
+    presentation.setEnabled(enabled);
+
+
+    if (enabled) {
+      presentation.setText(String.format(
+        "Partially Sync %s with %s", objectName, buildSystemName(e.getProject())
+      ));
+    } else {
+      presentation.setText(String.format("Partial %s Sync", buildSystemName(e.getProject())));
+    }
+  }
+
+  private static String buildSystemName(@Nullable Project project) {
+    return Blaze.buildSystemName(project);
+  }
+
+  @Nullable
+  private String getTargets(AnActionEvent e, List<TargetExpression> targets) {
+    Project project = e.getProject();
+    VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE);
+    if (project == null || virtualFile == null || !virtualFile.isInLocalFileSystem()) {
+      return null;
+    }
+
+    String objectName = null;
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+
+    if (virtualFile.isDirectory()) {
+      if (workspaceRoot.isInWorkspace(virtualFile)) {
+        targets.add(TargetExpression.allFromPackageRecursive(workspaceRoot.workspacePathFor(virtualFile)));
+      }
+      objectName = "Package";
+    } else {
+      targets.addAll(SourceToRuleMap.getInstance(project).getTargetsForSourceFile(new File(virtualFile.getPath())));
+
+      // If empty, try to build parent package
+      if (targets.isEmpty()) {
+        VirtualFile parent = virtualFile.getParent();
+        if (parent.isDirectory()) {
+          if (workspaceRoot.isInWorkspace(parent)) {
+            targets.add(TargetExpression.allFromPackageNonRecursive(workspaceRoot.workspacePathFor(parent)));
+          }
+        }
+      }
+      objectName = "File";
+    }
+
+    return objectName;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/actions/ShowPerformanceWarningsToggleAction.java b/blaze-base/src/com/google/idea/blaze/base/sync/actions/ShowPerformanceWarningsToggleAction.java
new file mode 100644
index 0000000..b5306ad
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/actions/ShowPerformanceWarningsToggleAction.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.actions;
+
+import com.google.idea.blaze.base.actions.BlazeToggleAction;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+
+/**
+ * Manages a tick box of whether to show performance warnings.
+ */
+public class ShowPerformanceWarningsToggleAction extends BlazeToggleAction {
+  @Override
+  public boolean isSelected(AnActionEvent e) {
+    return BlazeUserSettings.getInstance().getShowPerformanceWarnings();
+  }
+
+  @Override
+  public void setSelected(AnActionEvent e, boolean state) {
+    BlazeUserSettings.getInstance().setShowPerformanceWarnings(state);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/aspects/AspectStrategy.java b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/AspectStrategy.java
new file mode 100644
index 0000000..0d8e3e4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/AspectStrategy.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.aspects;
+
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
+import com.google.repackaged.protobuf.TextFormat;
+
+import java.io.*;
+
+/**
+ * Indirection for our various ways of calling the aspect.
+ */
+public interface AspectStrategy {
+
+  String getName();
+
+  void modifyIdeInfoCommand(BlazeCommand.Builder blazeCommandBuilder);
+
+  void modifyIdeResolveCommand(BlazeCommand.Builder blazeCommandBuilder);
+
+  String getAspectOutputFileExtension();
+
+  AndroidStudioIdeInfo.RuleIdeInfo readAspectFile(File file) throws IOException;
+
+  AspectStrategy NATIVE_ASPECT = new AspectStrategy() {
+    @Override
+    public String getName() {
+      return "NativeAspect";
+    }
+
+    @Override
+    public void modifyIdeInfoCommand(BlazeCommand.Builder blazeCommandBuilder) {
+      blazeCommandBuilder
+        .addBlazeFlags("--aspects=AndroidStudioInfoAspect")
+        .addBlazeFlags("--output_groups=ide-info");
+    }
+
+    @Override
+    public void modifyIdeResolveCommand(BlazeCommand.Builder blazeCommandBuilder) {
+      blazeCommandBuilder
+        .addBlazeFlags("--aspects=AndroidStudioInfoAspect")
+        .addBlazeFlags("--output_groups=ide-resolve");
+    }
+
+    @Override
+    public String getAspectOutputFileExtension() {
+      return ".aswb-build";
+    }
+
+    @Override
+    public AndroidStudioIdeInfo.RuleIdeInfo readAspectFile(File file) throws IOException {
+      try (InputStream inputStream = new FileInputStream(file)) {
+        return AndroidStudioIdeInfo.RuleIdeInfo.parseFrom(inputStream);
+      }
+    }
+  };
+
+  AspectStrategy SKYLARK_ASPECT = new AspectStrategy() {
+    @Override
+    public String getName() {
+      return "SkylarkAspect";
+    }
+
+    private void addAspectFlag(BlazeCommand.Builder blazeCommandBuilder) {
+      blazeCommandBuilder.addBlazeFlags(
+        "--aspects=//third_party/bazel/src/test/java/com/google/devtools/build/lib/ideinfo/intellij_info.bzl%intellij_info_aspect"
+      );
+    }
+
+    @Override
+    public void modifyIdeInfoCommand(BlazeCommand.Builder blazeCommandBuilder) {
+      addAspectFlag(blazeCommandBuilder);
+      blazeCommandBuilder.addBlazeFlags("--output_groups=ide-info-text");
+    }
+
+    @Override
+    public void modifyIdeResolveCommand(BlazeCommand.Builder blazeCommandBuilder) {
+      addAspectFlag(blazeCommandBuilder);
+      blazeCommandBuilder.addBlazeFlags("--output_groups=ide-resolve");
+    }
+
+    @Override
+    public String getAspectOutputFileExtension() {
+      return ".intellij-build.txt";
+    }
+
+    @Override
+    public AndroidStudioIdeInfo.RuleIdeInfo readAspectFile(File file) throws IOException {
+      try (InputStream inputStream = new FileInputStream(file)) {
+        AndroidStudioIdeInfo.RuleIdeInfo.Builder builder = AndroidStudioIdeInfo.RuleIdeInfo.newBuilder();
+        TextFormat.Parser parser = TextFormat.Parser.newBuilder()
+          .setAllowUnknownFields(true)
+          .build();
+        parser.merge(new InputStreamReader(inputStream), builder);
+        return builder.build();
+      }
+    }
+  };
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.java
new file mode 100644
index 0000000..d988982
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterface.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.base.sync.aspects;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.List;
+
+/**
+ * Indirection between ide_build_info and aspect style IDE info.
+ */
+public abstract class BlazeIdeInterface {
+
+  public static BlazeIdeInterface getInstance() {
+    return ServiceManager.getService(BlazeIdeInterface.class);
+  }
+
+  public static class IdeResult {
+    public final ImmutableMap<Label, RuleIdeInfo> ruleMap;
+    @Deprecated
+    @Nullable
+    public final File androidPlatformDirectory;
+    public IdeResult(
+      ImmutableMap<Label, RuleIdeInfo> ruleMap,
+      @Nullable File androidPlatformDirectory) {
+
+      this.ruleMap = ruleMap;
+      this.androidPlatformDirectory = androidPlatformDirectory;
+    }
+  }
+
+  @Nullable
+  public abstract IdeResult updateBlazeIdeState(
+    Project project,
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet,
+    List<TargetExpression> targets,
+    WorkspaceLanguageSettings workspaceLanguageSettings,
+    ArtifactLocationDecoder artifactLocationDecoder,
+    SyncState.Builder syncStateBuilder,
+    @Nullable SyncState previousSyncState,
+    boolean requiresAndroidSdk);
+
+  public abstract void resolveIdeArtifacts(
+    Project project,
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet,
+    List<TargetExpression> targets);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
new file mode 100644
index 0000000..367a1ed
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImpl.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.aspects;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.FutureUtil;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.ExperimentalShowArtifactsLineProcessor;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.prefetch.PrefetchService;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Result;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.ScopedFunction;
+import com.google.idea.blaze.base.scope.output.PerformanceWarning;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+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.filediff.FileDiffService;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Implementation of BlazeIdeInterface based on aspects.
+ */
+public class BlazeIdeInterfaceAspectsImpl extends BlazeIdeInterface {
+
+  private static final Logger LOG = Logger.getInstance(BlazeIdeInterfaceAspectsImpl.class);
+  private static final Label ANDROID_SDK_TARGET = new Label("//third_party/java/android/android_sdk_linux:android");
+  private static final FileDiffService fileDiffService = new FileDiffService();
+  private static final BoolExperiment USE_SKYLARK_ASPECT = new BoolExperiment("use.skylark.aspect", false);
+
+  static class State implements Serializable {
+    private static final long serialVersionUID = 10L;
+    ImmutableMap<Label, RuleIdeInfo> ruleMap;
+    File androidPlatformDirectory;
+    FileDiffService.State fileState = null;
+    Map<File, Label> fileToLabel = Maps.newHashMap();
+    WorkspaceLanguageSettings workspaceLanguageSettings;
+    String aspectStrategyName;
+  }
+
+  @Nullable
+  @Override
+  public IdeResult updateBlazeIdeState(Project project,
+                                       BlazeContext context,
+                                       WorkspaceRoot workspaceRoot,
+                                       ProjectViewSet projectViewSet,
+                                       List<TargetExpression> targets,
+                                       WorkspaceLanguageSettings workspaceLanguageSettings,
+                                       ArtifactLocationDecoder artifactLocationDecoder,
+                                       SyncState.Builder syncStateBuilder,
+                                       @Nullable SyncState previousSyncState,
+                                       boolean requiresAndroidSdk) {
+    State prevState = previousSyncState != null ? previousSyncState.get(State.class) : null;
+
+    // If the language filter has changed, redo everything from scratch
+    if (prevState != null && !prevState.workspaceLanguageSettings.equals(workspaceLanguageSettings)) {
+      prevState = null;
+    }
+
+    // If the aspect strategy has changed, redo everything from scratch
+    final AspectStrategy aspectStrategy = getAspectStrategy();
+    if (prevState != null && !Objects.equal(prevState.aspectStrategyName, aspectStrategy.getName())) {
+      prevState = null;
+    }
+
+    List<File> fileList = getIdeInfo(project, context, workspaceRoot, projectViewSet, targets, aspectStrategy, requiresAndroidSdk);
+    if (!context.shouldContinue()) {
+      return null;
+    }
+
+    List<File> updatedFiles = Lists.newArrayList();
+    List<File> removedFiles = Lists.newArrayList();
+    FileDiffService.State fileState = fileDiffService.updateFiles(
+      prevState != null ? prevState.fileState : null,
+      fileList,
+      updatedFiles,
+      removedFiles
+    );
+    if (fileState == null) {
+      return null;
+    }
+
+    context.output(new PrintOutput(String.format(
+      "Total rules: %d, new/changed: %d, removed: %d",
+      fileList.size(),
+      updatedFiles.size(),
+      removedFiles.size()
+    )));
+
+    ListenableFuture<?> prefetchFuture = PrefetchService.getInstance().prefetchFiles(updatedFiles, true);
+    if (!FutureUtil.waitForFuture(context, prefetchFuture)
+      .timed("FetchAspectOutput")
+      .run()
+      .success()) {
+      return null;
+    }
+
+    State state = updateState(
+      context,
+      prevState,
+      fileState,
+      workspaceLanguageSettings,
+      artifactLocationDecoder,
+      aspectStrategy,
+      updatedFiles,
+      removedFiles
+    );
+    if (state == null) {
+      return null;
+    }
+    if (state.androidPlatformDirectory == null && requiresAndroidSdk) {
+      LOG.error("Android platform directory not found.");
+      return null;
+    }
+    syncStateBuilder.put(State.class, state);
+    return new IdeResult(state.ruleMap, state.androidPlatformDirectory);
+  }
+
+  private static List<File> getIdeInfo(Project project,
+                                       BlazeContext parentContext,
+                                       WorkspaceRoot workspaceRoot,
+                                       ProjectViewSet projectViewSet,
+                                       List<TargetExpression> targets,
+                                       AspectStrategy aspectStrategy,
+                                       boolean addAndroidSdkTarget) {
+    return Scope.push(parentContext, context -> {
+      context.push(new TimingScope("ExecuteBlazeCommand"));
+
+      List<File> result = Lists.newArrayList();
+
+      BuildSystem buildSystem = Blaze.getBuildSystem(project);
+      BlazeCommand.Builder blazeCommandBuilder = BlazeCommand.builder(buildSystem, BlazeCommandName.BUILD);
+      if (addAndroidSdkTarget) {
+        blazeCommandBuilder.addTargets(ANDROID_SDK_TARGET);
+      }
+      blazeCommandBuilder
+        .addTargets(targets)
+        .addBlazeFlags(BlazeFlags.EXPERIMENTAL_SHOW_ARTIFACTS)
+        .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));
+
+      aspectStrategy.modifyIdeInfoCommand(blazeCommandBuilder);
+
+      int retVal = ExternalTask.builder(workspaceRoot, blazeCommandBuilder.build())
+        .context(context)
+        .stderr(LineProcessingOutputStream.of(
+          new ExperimentalShowArtifactsLineProcessor(result, aspectStrategy.getAspectOutputFileExtension()),
+          new IssueOutputLineProcessor(project, context, workspaceRoot)
+        ))
+        .build()
+        .run(new LoggedTimingScope(project, Action.BLAZE_BUILD));
+
+      if (retVal != 0) {
+        context.setHasError();
+      }
+
+      return result;
+    });
+  }
+
+  private static class RuleIdeInfoOrSdkInfo {
+    public File file;
+    public RuleIdeInfo ruleIdeInfo;
+    public File androidPlatformDirectory;
+  }
+
+  @Nullable
+  static State updateState(BlazeContext parentContext,
+                           @Nullable State prevState,
+                           FileDiffService.State fileState,
+                           WorkspaceLanguageSettings workspaceLanguageSettings,
+                           ArtifactLocationDecoder artifactLocationDecoder,
+                           AspectStrategy aspectStrategy,
+                           List<File> newFiles,
+                           List<File> removedFiles) {
+    Result<State> result = Scope.push(parentContext, (ScopedFunction<Result<State>>)context -> {
+      context.push(new TimingScope("UpdateRuleMap"));
+
+      State state = new State();
+      state.fileState = fileState;
+      state.workspaceLanguageSettings = workspaceLanguageSettings;
+      state.aspectStrategyName = aspectStrategy.getName();
+
+      Map<Label, RuleIdeInfo> ruleMap = Maps.newHashMap();
+      Map<Label, RuleIdeInfo> updatedRules = Maps.newHashMap();
+      if (prevState != null) {
+        ruleMap.putAll(prevState.ruleMap);
+        state.androidPlatformDirectory = prevState.androidPlatformDirectory;
+        state.fileToLabel.putAll(prevState.fileToLabel);
+      }
+
+      // Update removed
+      for (File removedFile : removedFiles) {
+        Label label = state.fileToLabel.remove(removedFile);
+        if (label != null) {
+          ruleMap.remove(label);
+        }
+      }
+
+      AtomicLong totalSizeLoaded = new AtomicLong(0);
+
+      // Read protos from any new files
+      List<ListenableFuture<RuleIdeInfoOrSdkInfo>> futures = Lists.newArrayList();
+      for (File file : newFiles) {
+        futures.add(submit(() -> {
+          RuleIdeInfoOrSdkInfo ruleIdeInfoOrSdkInfo = new RuleIdeInfoOrSdkInfo();
+          ruleIdeInfoOrSdkInfo.file = file;
+
+          totalSizeLoaded.addAndGet(file.length());
+
+          AndroidStudioIdeInfo.RuleIdeInfo ruleProto = aspectStrategy.readAspectFile(file);
+          if (ruleProto.getLabel().equals(ANDROID_SDK_TARGET.toString())) {
+            ruleIdeInfoOrSdkInfo.androidPlatformDirectory = getAndroidPlatformDirectoryFromAndroidTarget(
+              ruleProto,
+              artifactLocationDecoder
+            );
+          }
+          else {
+            ruleIdeInfoOrSdkInfo.ruleIdeInfo = IdeInfoFromProtobuf.makeRuleIdeInfo(
+              workspaceLanguageSettings,
+              artifactLocationDecoder,
+              ruleProto
+            );
+          }
+          return ruleIdeInfoOrSdkInfo;
+        }
+        ));
+      }
+
+      // Update state with result from proto files
+      int duplicateRuleLabels = 0;
+      try {
+        for (RuleIdeInfoOrSdkInfo ruleIdeInfoOrSdkInfo : Futures.allAsList(futures).get()) {
+          if (ruleIdeInfoOrSdkInfo.androidPlatformDirectory != null) {
+            state.androidPlatformDirectory = ruleIdeInfoOrSdkInfo.androidPlatformDirectory;
+          } else if (ruleIdeInfoOrSdkInfo.ruleIdeInfo != null) {
+            File file = ruleIdeInfoOrSdkInfo.file;
+            Label label = ruleIdeInfoOrSdkInfo.ruleIdeInfo.label;
+
+            RuleIdeInfo previousRule = updatedRules.putIfAbsent(label, ruleIdeInfoOrSdkInfo.ruleIdeInfo);
+            if (previousRule == null) {
+              state.fileToLabel.put(file, label);
+            } else {
+              duplicateRuleLabels++;
+            }
+          }
+        }
+      }
+      catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
+        return Result.error(null);
+      }
+      catch (ExecutionException e) {
+        return Result.error(e);
+      }
+      ruleMap.putAll(updatedRules);
+
+      context.output(new PrintOutput(String.format(
+        "Loaded %d aspect files, total size %dkB", newFiles.size(), totalSizeLoaded.get() / 1024
+      )));
+      if (duplicateRuleLabels > 0) {
+        context.output(new PerformanceWarning(String.format(
+          "There were %d duplicate rules. You may be including multiple configurations in your build. "
+          + "Your IDE sync is slowed down by ~%d%%.",
+          duplicateRuleLabels,
+          (100 * duplicateRuleLabels / ruleMap.size())
+        )));
+      }
+
+      state.ruleMap = ImmutableMap.copyOf(ruleMap);
+      return Result.of(state);
+    });
+
+    if (result.error != null) {
+      LOG.error(result.error);
+      return null;
+    }
+    return result.result;
+  }
+
+  @Nullable
+  private static File getAndroidPlatformDirectoryFromAndroidTarget(AndroidStudioIdeInfo.RuleIdeInfo ruleProto,
+                                                                   ArtifactLocationDecoder artifactLocationDecoder) {
+    if (!ruleProto.hasJavaRuleIdeInfo()) {
+      return null;
+    }
+    AndroidStudioIdeInfo.JavaRuleIdeInfo javaRuleIdeInfo = ruleProto.getJavaRuleIdeInfo();
+    if (javaRuleIdeInfo.getJarsCount() == 0) {
+      return null;
+    }
+    AndroidStudioIdeInfo.LibraryArtifact libraryArtifact = javaRuleIdeInfo.getJars(0);
+    AndroidStudioIdeInfo.ArtifactLocation artifactLocation = libraryArtifact.getJar();
+    if (artifactLocation == null) {
+      return null;
+    }
+    File androidJar = artifactLocationDecoder.decode(artifactLocation).getFile();
+    return androidJar.getParentFile();
+  }
+
+  private static <T> ListenableFuture<T> submit(Callable<T> callable) {
+    return BlazeExecutor.getInstance().submit(callable);
+  }
+
+  @Override
+  public void resolveIdeArtifacts(Project project,
+                                  BlazeContext context,
+                                  WorkspaceRoot workspaceRoot,
+                                  ProjectViewSet projectViewSet,
+                                  List<TargetExpression> targets) {
+    AspectStrategy aspectStrategy = getAspectStrategy();
+
+    BlazeCommand.Builder blazeCommandBuilder = BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.BUILD)
+      .addTargets(targets)
+      .addBlazeFlags()
+      .addBlazeFlags(BlazeFlags.KEEP_GOING)
+      .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet));
+
+    aspectStrategy.modifyIdeResolveCommand(blazeCommandBuilder);
+
+    BlazeCommand blazeCommand = blazeCommandBuilder.build();
+
+    int retVal = ExternalTask.builder(workspaceRoot, blazeCommand)
+      .context(context)
+      .stderr(LineProcessingOutputStream.of(new IssueOutputLineProcessor(project, context, workspaceRoot)))
+      .build()
+      .run(new LoggedTimingScope(project, Action.BLAZE_BUILD));
+
+    if (retVal != 0) {
+      context.setHasError();
+    }
+  }
+
+  private AspectStrategy getAspectStrategy() {
+    return USE_SKYLARK_ASPECT.getValue() ? AspectStrategy.SKYLARK_ASPECT : AspectStrategy.NATIVE_ASPECT;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
new file mode 100644
index 0000000..e723505
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/IdeInfoFromProtobuf.java
@@ -0,0 +1,325 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.idea.blaze.base.sync.aspects;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.ideinfo.*;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
+import com.google.repackaged.protobuf.ProtocolStringList;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Conversion functions from new aspect-style Bazel IDE info to ASWB internal classes.
+ */
+public class IdeInfoFromProtobuf {
+
+  @Nullable
+  public static RuleIdeInfo makeRuleIdeInfo(WorkspaceLanguageSettings workspaceLanguageSettings,
+                                            ArtifactLocationDecoder decoder,
+                                            AndroidStudioIdeInfo.RuleIdeInfo message) {
+    Kind kind = getKind(message);
+    if (kind == null) {
+      return null;
+    }
+    if (!workspaceLanguageSettings.isLanguageActive(kind.getLanguageClass())) {
+      return null;
+    }
+
+    Label label = new Label(message.getLabel());
+    ArtifactLocation buildFile = getBuildFile(decoder, message);
+
+    Collection<Label> dependencies = makeLabelListFromProtobuf(message.getDependenciesList());
+    Collection<Label> runtimeDeps = makeLabelListFromProtobuf(message.getRuntimeDepsList());
+    Collection<String> tags = ImmutableList.copyOf(message.getTagsList());
+
+    Collection<ArtifactLocation> sources = Lists.newArrayList();
+    CRuleIdeInfo cRuleIdeInfo = null;
+    if (message.hasCRuleIdeInfo()) {
+      cRuleIdeInfo = makeCRuleIdeInfo(decoder, message.getCRuleIdeInfo());
+      sources.addAll(cRuleIdeInfo.sources);
+    }
+    CToolchainIdeInfo cToolchainIdeInfo = null;
+    if (message.hasCToolchainIdeInfo()) {
+      cToolchainIdeInfo = makeCToolchainIdeInfo(message.getCToolchainIdeInfo());
+    }
+    JavaRuleIdeInfo javaRuleIdeInfo = null;
+    if (message.hasJavaRuleIdeInfo()) {
+      javaRuleIdeInfo = makeJavaRuleIdeInfo(decoder, message.getJavaRuleIdeInfo());
+      Collection<ArtifactLocation> javaSources = makeArtifactLocationList(decoder, message.getJavaRuleIdeInfo().getSourcesList());
+      sources.addAll(javaSources);
+    }
+    AndroidRuleIdeInfo androidRuleIdeInfo = null;
+    if (message.hasAndroidRuleIdeInfo()) {
+      androidRuleIdeInfo = makeAndroidRuleIdeInfo(decoder, message.getAndroidRuleIdeInfo());
+    }
+    TestIdeInfo testIdeInfo = null;
+    if (message.hasTestInfo()) {
+      testIdeInfo = makeTestIdeInfo(message.getTestInfo());
+    }
+    ProtoLibraryLegacyInfo protoLibraryLegacyInfo = null;
+    if (message.hasProtoLibraryLegacyJavaIdeInfo()) {
+      protoLibraryLegacyInfo = makeProtoLibraryLegacyInfo(decoder, message.getProtoLibraryLegacyJavaIdeInfo());
+    }
+    JavaToolchainIdeInfo javaToolchainIdeInfo = null;
+    if (message.hasJavaToolchainIdeInfo()) {
+      javaToolchainIdeInfo = makeJavaToolchainIdeInfo(message.getJavaToolchainIdeInfo());
+    }
+
+    return new RuleIdeInfo(
+      label,
+      kind,
+      buildFile,
+      dependencies,
+      runtimeDeps,
+      tags,
+      sources,
+      cRuleIdeInfo,
+      cToolchainIdeInfo,
+      javaRuleIdeInfo,
+      androidRuleIdeInfo,
+      testIdeInfo,
+      protoLibraryLegacyInfo,
+      javaToolchainIdeInfo
+    );
+  }
+
+  @Nullable
+  private static ArtifactLocation getBuildFile(ArtifactLocationDecoder decoder,
+                                               AndroidStudioIdeInfo.RuleIdeInfo message) {
+    if (message.hasBuildFileArtifactLocation()) {
+      return makeArtifactLocation(decoder, message.getBuildFileArtifactLocation());
+    }
+    return null;
+  }
+
+  private static CRuleIdeInfo makeCRuleIdeInfo(
+    ArtifactLocationDecoder decoder,
+    AndroidStudioIdeInfo.CRuleIdeInfo cRuleIdeInfo
+  ) {
+    List<ArtifactLocation> sources = makeArtifactLocationList(decoder, cRuleIdeInfo.getSourceList());
+    List<ExecutionRootPath> transitiveIncludeDirectories = makeExecutionRootPathList(cRuleIdeInfo.getTransitiveIncludeDirectoryList());
+    List<ExecutionRootPath> transitiveQuoteIncludeDirectories =
+      makeExecutionRootPathList(cRuleIdeInfo.getTransitiveQuoteIncludeDirectoryList());
+    List<ExecutionRootPath> transitiveSystemIncludeDirectories =
+      makeExecutionRootPathList(cRuleIdeInfo.getTransitiveSystemIncludeDirectoryList());
+
+    CRuleIdeInfo.Builder builder = CRuleIdeInfo.builder()
+      .addSources(sources)
+      .addTransitiveIncludeDirectories(transitiveIncludeDirectories)
+      .addTransitiveQuoteIncludeDirectories(transitiveQuoteIncludeDirectories)
+      .addTransitiveDefines(cRuleIdeInfo.getTransitiveDefineList())
+      .addTransitiveSystemIncludeDirectories(transitiveSystemIncludeDirectories)
+    ;
+
+    return builder.build();
+  }
+
+  private static List<ExecutionRootPath> makeExecutionRootPathList(Iterable<String> relativePaths) {
+    List<ExecutionRootPath> workspacePaths = Lists.newArrayList();
+    for (String relativePath : relativePaths) {
+      workspacePaths.add(new ExecutionRootPath(relativePath));
+    }
+    return workspacePaths;
+  }
+
+  private static CToolchainIdeInfo makeCToolchainIdeInfo(AndroidStudioIdeInfo.CToolchainIdeInfo cToolchainIdeInfo) {
+    Collection<ExecutionRootPath> builtInIncludeDirectories = makeExecutionRootPathList(cToolchainIdeInfo.getBuiltInIncludeDirectoryList());
+    ExecutionRootPath cppExecutable = new ExecutionRootPath(cToolchainIdeInfo.getCppExecutable());
+    ExecutionRootPath preprocessorExecutable = new ExecutionRootPath(cToolchainIdeInfo.getPreprocessorExecutable());
+
+    UnfilteredCompilerOptions unfilteredCompilerOptions =
+      new UnfilteredCompilerOptions(cToolchainIdeInfo.getUnfilteredCompilerOptionList());
+
+    CToolchainIdeInfo.Builder builder = CToolchainIdeInfo.builder()
+      .addBaseCompilerOptions(cToolchainIdeInfo.getBaseCompilerOptionList())
+      .addCCompilerOptions(cToolchainIdeInfo.getCOptionList())
+      .addCppCompilerOptions(cToolchainIdeInfo.getCppOptionList())
+      .addLinkOptions(cToolchainIdeInfo.getLinkOptionList())
+      .addBuiltInIncludeDirectories(builtInIncludeDirectories)
+      .setCppExecutable(cppExecutable)
+      .setPreprocessorExecutable(preprocessorExecutable)
+      .setTargetName(cToolchainIdeInfo.getTargetName())
+      .addUnfilteredCompilerOptions(unfilteredCompilerOptions.getToolchainFlags())
+      .addUnfilteredToolchainSystemIncludes(unfilteredCompilerOptions.getToolchainSysIncludes())
+      ;
+
+    return builder.build();
+  }
+
+  private static JavaRuleIdeInfo makeJavaRuleIdeInfo(ArtifactLocationDecoder decoder,
+                                                     AndroidStudioIdeInfo.JavaRuleIdeInfo javaRuleIdeInfo) {
+    return new JavaRuleIdeInfo(
+      makeLibraryArtifactList(decoder, javaRuleIdeInfo.getJarsList()),
+      makeLibraryArtifactList(decoder, javaRuleIdeInfo.getGeneratedJarsList()),
+      javaRuleIdeInfo.hasPackageManifest() ? makeArtifactLocation(decoder, javaRuleIdeInfo.getPackageManifest()) : null,
+      javaRuleIdeInfo.hasJdeps() ? makeArtifactLocation(decoder, javaRuleIdeInfo.getJdeps()) : null
+    );
+  }
+
+  private static AndroidRuleIdeInfo makeAndroidRuleIdeInfo(ArtifactLocationDecoder decoder,
+                                                           AndroidStudioIdeInfo.AndroidRuleIdeInfo androidRuleIdeInfo) {
+    return new AndroidRuleIdeInfo(
+      makeArtifactLocationList(decoder, androidRuleIdeInfo.getResourcesList()),
+      androidRuleIdeInfo.getJavaPackage(),
+      androidRuleIdeInfo.getGenerateResourceClass(),
+      androidRuleIdeInfo.hasManifest() ? makeArtifactLocation(decoder, androidRuleIdeInfo.getManifest()) : null,
+      androidRuleIdeInfo.hasIdlJar() ? makeLibraryArtifact(decoder, androidRuleIdeInfo.getIdlJar()) : null,
+      androidRuleIdeInfo.hasResourceJar() ? makeLibraryArtifact(decoder, androidRuleIdeInfo.getResourceJar()) : null,
+      androidRuleIdeInfo.getHasIdlSources(),
+      !Strings.isNullOrEmpty(androidRuleIdeInfo.getLegacyResources()) ? new Label(androidRuleIdeInfo.getLegacyResources()) : null
+    );
+  }
+
+  private static TestIdeInfo makeTestIdeInfo(AndroidStudioIdeInfo.TestInfo testInfo) {
+    String size = testInfo.getSize();
+    TestIdeInfo.TestSize testSize = TestIdeInfo.DEFAULT_RULE_TEST_SIZE;
+    if (!Strings.isNullOrEmpty(size)) {
+      switch (size) {
+        case "small":
+          testSize = TestIdeInfo.TestSize.SMALL;
+          break;
+        case "medium":
+          testSize = TestIdeInfo.TestSize.MEDIUM;
+          break;
+        case "large":
+          testSize = TestIdeInfo.TestSize.LARGE;
+          break;
+        case "enormous":
+          testSize = TestIdeInfo.TestSize.ENORMOUS;
+          break;
+        default:
+          break;
+      }
+    }
+    return new TestIdeInfo(testSize);
+  }
+
+  private static ProtoLibraryLegacyInfo makeProtoLibraryLegacyInfo(ArtifactLocationDecoder decoder,
+                                                                   AndroidStudioIdeInfo.ProtoLibraryLegacyJavaIdeInfo protoLibraryLegacyJavaIdeInfo) {
+    final ProtoLibraryLegacyInfo.ApiFlavor apiFlavor;
+    if (protoLibraryLegacyJavaIdeInfo.getApiVersion() == 1) {
+      apiFlavor = ProtoLibraryLegacyInfo.ApiFlavor.VERSION_1;
+    } else {
+      switch (protoLibraryLegacyJavaIdeInfo.getApiFlavor()) {
+        case MUTABLE:
+          apiFlavor = ProtoLibraryLegacyInfo.ApiFlavor.MUTABLE;
+          break;
+        case IMMUTABLE:
+          apiFlavor = ProtoLibraryLegacyInfo.ApiFlavor.IMMUTABLE;
+          break;
+        case BOTH:
+          apiFlavor = ProtoLibraryLegacyInfo.ApiFlavor.BOTH;
+          break;
+        default:
+          apiFlavor = ProtoLibraryLegacyInfo.ApiFlavor.NONE;
+          break;
+      }
+    }
+    return new ProtoLibraryLegacyInfo(
+      apiFlavor,
+      makeLibraryArtifactList(decoder, protoLibraryLegacyJavaIdeInfo.getJars1List()),
+      makeLibraryArtifactList(decoder, protoLibraryLegacyJavaIdeInfo.getJarsMutableList()),
+      makeLibraryArtifactList(decoder, protoLibraryLegacyJavaIdeInfo.getJarsImmutableList())
+    );
+  }
+
+  private static JavaToolchainIdeInfo makeJavaToolchainIdeInfo(AndroidStudioIdeInfo.JavaToolchainIdeInfo javaToolchainIdeInfo) {
+    return new JavaToolchainIdeInfo(javaToolchainIdeInfo.getSourceVersion(), javaToolchainIdeInfo.getTargetVersion());
+  }
+
+  private static Collection<LibraryArtifact> makeLibraryArtifactList(
+    ArtifactLocationDecoder decoder,
+    List<AndroidStudioIdeInfo.LibraryArtifact> jarsList) {
+    ImmutableList.Builder<LibraryArtifact> builder = ImmutableList.builder();
+    for (AndroidStudioIdeInfo.LibraryArtifact libraryArtifact : jarsList) {
+      LibraryArtifact lib = makeLibraryArtifact(decoder, libraryArtifact);
+      if (lib != null) {
+        builder.add(lib);
+      }
+    }
+    return builder.build();
+  }
+
+  @Nullable
+  private static LibraryArtifact makeLibraryArtifact(ArtifactLocationDecoder decoder,
+                                                     AndroidStudioIdeInfo.LibraryArtifact libraryArtifact) {
+    ArtifactLocation runtimeJar = libraryArtifact.hasJar()
+                                  ? makeArtifactLocation(decoder, libraryArtifact.getJar()) : null;
+    ArtifactLocation iJar = libraryArtifact.hasInterfaceJar()
+                            ? makeArtifactLocation(decoder, libraryArtifact.getInterfaceJar()) : runtimeJar;
+    ArtifactLocation sourceJar = libraryArtifact.hasSourceJar()
+                                 ? makeArtifactLocation(decoder, libraryArtifact.getSourceJar()) : null;
+    if (iJar == null) {
+      // Failed to find ArtifactLocation file -- presumably because it was removed from file system since blaze build
+      return null;
+    }
+    return new LibraryArtifact(
+      iJar,
+      runtimeJar,
+      sourceJar
+    );
+  }
+
+  private static List<ArtifactLocation> makeArtifactLocationList(
+    ArtifactLocationDecoder decoder,
+    List<AndroidStudioIdeInfo.ArtifactLocation> sourcesList) {
+    ImmutableList.Builder<ArtifactLocation> builder = ImmutableList.builder();
+    for (AndroidStudioIdeInfo.ArtifactLocation pbArtifactLocation : sourcesList) {
+      ArtifactLocation loc = makeArtifactLocation(decoder, pbArtifactLocation);
+      if (loc != null) {
+        builder.add(loc);
+      }
+    }
+    return builder.build();
+  }
+
+  @Nullable
+  private static ArtifactLocation makeArtifactLocation(ArtifactLocationDecoder decoder,
+                                                       AndroidStudioIdeInfo.ArtifactLocation pbArtifactLocation) {
+    if (pbArtifactLocation == null) {
+      return null;
+    }
+    return decoder.decode(pbArtifactLocation);
+  }
+
+  private static Collection<Label> makeLabelListFromProtobuf(ProtocolStringList dependenciesList) {
+    ImmutableList.Builder<Label> dependenciesBuilder = ImmutableList.builder();
+    for (String dependencyLabel : dependenciesList) {
+      dependenciesBuilder.add(new Label(dependencyLabel));
+    }
+    return dependenciesBuilder.build();
+  }
+
+  @Nullable
+  private static Kind getKind(AndroidStudioIdeInfo.RuleIdeInfo rule) {
+    String kindString = rule.getKindString();
+    if (!Strings.isNullOrEmpty(kindString)) {
+      return Kind.fromString(kindString);
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptions.java b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptions.java
new file mode 100644
index 0000000..4345b6f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptions.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.aspects;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+
+import java.util.List;
+
+/**
+ * unfilteredCompilerOptions is a grab bag of options passed to the compiler. Do minimal parsing to extract what we need.
+ */
+final class UnfilteredCompilerOptions {
+  private enum NextOption {ISYSTEM, FLAG}
+
+  private final List<ExecutionRootPath> toolchainSysIncludes;
+  private final List<String> toolchainFlags;
+
+  public UnfilteredCompilerOptions(Iterable<String> unfilteredOptions) {
+    List<String> toolchainSystemIncludePaths = Lists.newArrayList();
+    toolchainFlags = Lists.newArrayList();
+    splitUnfilteredCompilerOptions(unfilteredOptions, toolchainSystemIncludePaths, toolchainFlags);
+
+    toolchainSysIncludes = Lists.newArrayList();
+    for (String systemInclude : toolchainSystemIncludePaths) {
+      toolchainSysIncludes.add(new ExecutionRootPath(systemInclude));
+    }
+  }
+
+  public List<String> getToolchainFlags() {
+    return toolchainFlags;
+  }
+
+  public List<ExecutionRootPath> getToolchainSysIncludes() {
+    return toolchainSysIncludes;
+  }
+
+  @VisibleForTesting
+  static void splitUnfilteredCompilerOptions(
+    Iterable<String> unfilteredOptions,
+    List<String> toolchainSysIncludes,
+    List<String> toolchainFlags
+  ) {
+    NextOption nextOption = NextOption.FLAG;
+    for (String unfilteredOption : unfilteredOptions) {
+      // We are looking for either the flag pair "-isystem /path/to/dir" or the flag "-isystem/path/to/dir"
+      //
+      // blaze emits isystem flags in both formats. The latter isn't ideal but apparently it is accepted by GCC and will be emitted by
+      // blaze under certain circumstances.
+      if (nextOption == NextOption.ISYSTEM) {
+        toolchainSysIncludes.add(unfilteredOption);
+        nextOption = NextOption.FLAG;
+      }
+      else {
+        if (unfilteredOption.equals("-isystem")) {
+          nextOption = NextOption.ISYSTEM;
+        }
+        else if (unfilteredOption.startsWith("-isystem")) {
+          String iSystemIncludePath = unfilteredOption.substring("-isystem".length());
+          toolchainSysIncludes.add(iSystemIncludePath);
+        }
+        else {
+          toolchainFlags.add(unfilteredOption);
+          nextOption = NextOption.FLAG;
+        }
+      }
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeDataStorage.java b/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeDataStorage.java
new file mode 100644
index 0000000..edb49ea
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeDataStorage.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.data;
+
+import com.google.common.base.Strings;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * Defines where we store our blaze project data.
+ */
+public class BlazeDataStorage {
+  public static final String CACHE_FILE_NAME = "cache.dat";
+
+  @NotNull
+  public static File getProjectCacheDir(
+    @NotNull Project project,
+    @NotNull BlazeImportSettings importSettings) {
+    String locationHash = importSettings.getLocationHash();
+
+    // Legacy support: The location hash used to be just the project hash
+    if (Strings.isNullOrEmpty(locationHash)) {
+      locationHash = project.getLocationHash();
+    }
+
+    return new File(getProjectConfigurationDir(), locationHash);
+  }
+
+  private static File getProjectConfigurationDir() {
+    return new File(PathManager.getSystemPath(), "blaze/projects").getAbsoluteFile();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManager.java b/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManager.java
new file mode 100644
index 0000000..fa0f881
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManager.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.data;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+/**
+ * Stores a cache of blaze project data.
+ */
+public interface BlazeProjectDataManager {
+  static BlazeProjectDataManager getInstance(Project project) {
+    return ServiceManager.getService(project, BlazeProjectDataManager.class);
+  }
+
+  @Nullable
+  BlazeProjectData getBlazeProjectData();
+
+  BlazeSyncPlugin.ModuleEditor editModules();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManagerImpl.java b/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManagerImpl.java
new file mode 100644
index 0000000..c10640a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/data/BlazeProjectDataManagerImpl.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.data;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.StatusOutput;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider;
+import com.google.idea.blaze.base.util.SerializationUtil;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.progress.ProgressIndicator;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Stores a cache of blaze project data and issues any side effects when that data is updated.
+ */
+public class BlazeProjectDataManagerImpl implements BlazeProjectDataManager {
+
+  private static final Logger LOG = Logger.getInstance(BlazeProjectDataManagerImpl.class.getName());
+
+  private final Project project;
+
+  @Nullable
+  private volatile BlazeProjectData blazeProjectData;
+
+  private final Object saveLock = new Object();
+
+  public static BlazeProjectDataManagerImpl getImpl(Project project) {
+    return (BlazeProjectDataManagerImpl) BlazeProjectDataManager.getInstance(project);
+  }
+
+  public BlazeProjectDataManagerImpl(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  public BlazeProjectData loadProjectRoot(
+    BlazeContext context,
+    BlazeImportSettings importSettings) {
+    BlazeProjectData projectData = blazeProjectData;
+    if (projectData != null) {
+      return projectData;
+    }
+    synchronized (this) {
+      projectData = blazeProjectData;
+      return projectData != null ? projectData : loadProject(context, importSettings);
+    }
+  }
+
+  @Override
+  @Nullable
+  public BlazeProjectData getBlazeProjectData() {
+    return blazeProjectData;
+  }
+
+  @Override
+  public BlazeSyncPlugin.ModuleEditor editModules() {
+    return ModuleEditorProvider.getInstance().getModuleEditor(
+      project,
+      BlazeImportSettingsManager.getInstance(project).getImportSettings()
+    );
+  }
+
+  @Nullable
+  private synchronized BlazeProjectData loadProject(
+    BlazeContext context,
+    BlazeImportSettings importSettings) {
+    BlazeProjectData blazeProjectData = null;
+    try {
+      File file = getCacheFile(project, importSettings);
+
+      List<ClassLoader> classLoaders = Lists.newArrayList();
+      for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+        classLoaders.add(syncPlugin.getClass().getClassLoader());
+      }
+      classLoaders.add(getClass().getClassLoader());
+      classLoaders.add(Thread.currentThread().getContextClassLoader());
+
+      blazeProjectData = (BlazeProjectData)SerializationUtil.loadFromDisk(file, classLoaders);
+    }
+    catch (IOException e) {
+      String buildSystemName = importSettings.getBuildSystem().getLowerCaseName();
+      context.output(new StatusOutput(String.format("Stale %s project cache, sync will be needed", buildSystemName)));
+      LOG.info(e);
+    }
+
+    this.blazeProjectData = blazeProjectData;
+    return blazeProjectData;
+  }
+
+  public void saveProject(
+    final BlazeImportSettings importSettings,
+    final BlazeProjectData blazeProjectData) {
+    this.blazeProjectData = blazeProjectData;
+
+    // Can only run one save operation per project at a time
+    synchronized (saveLock) {
+      BlazeExecutor.submitTask(project, "Saving sync data...", (ProgressIndicator indicator) -> {
+        try {
+          File file = getCacheFile(project, importSettings);
+          SerializationUtil.saveToDisk(file, blazeProjectData);
+        }
+        catch (IOException e) {
+          LOG.error("Could not save cache data file to disk. Please resync project. Error: " + e.getMessage());
+        }
+      });
+    }
+  }
+
+  private static File getCacheFile(Project project, BlazeImportSettings importSettings) {
+    return new File(BlazeDataStorage.getProjectCacheDir(project, importSettings), BlazeDataStorage.CACHE_FILE_NAME);
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/filediff/FileDiffService.java b/blaze-base/src/com/google/idea/blaze/base/sync/filediff/FileDiffService.java
new file mode 100644
index 0000000..9cb4854
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/filediff/FileDiffService.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.filediff;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+/**
+ * Provides a diffing service for a collection of files.
+ */
+public class FileDiffService {
+  private static Logger LOG = Logger.getInstance(FileDiffService.class);
+
+  public static class State implements Serializable {
+    private static final long serialVersionUID = 2L;
+    Map<File, FileEntry> fileEntryMap;
+  }
+
+  static class FileEntry implements Serializable {
+    private static final long serialVersionUID = 2L;
+
+    public File file;
+    public long timestamp;
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (o == null || getClass() != o.getClass()) return false;
+      FileEntry fileEntry = (FileEntry)o;
+      return Objects.equal(timestamp, fileEntry.timestamp) &&
+             Objects.equal(file, fileEntry.file);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(file, timestamp);
+    }
+  }
+
+  @Nullable
+  public State updateFiles(@Nullable State oldState,
+                           @NotNull Iterable<File> files,
+                           @NotNull List<File> updatedFiles,
+                           @NotNull List<File> removedFiles) {
+    Map<File, FileEntry> oldFiles = oldState != null
+                                    ? oldState.fileEntryMap
+                                    : ImmutableMap.of();
+
+    List<FileEntry> fileEntryList = null;
+    try {
+      fileEntryList = updateTimeStamps(files);
+    } catch (Exception e) {
+      LOG.error(e);
+      return null;
+    }
+
+    // Find changed/new
+    for (FileEntry newFile : fileEntryList) {
+      FileEntry oldFile = oldFiles.get(newFile.file);
+      final boolean isNew = oldFile == null || newFile.timestamp != oldFile.timestamp;
+      if (isNew) {
+        updatedFiles.add(newFile.file);
+      }
+    }
+
+    // Find removed
+    Set<File> newFiles = Sets.newHashSet();
+    for (File file : files) {
+      newFiles.add(file);
+    }
+    for (File file : oldFiles.keySet()) {
+      if (!newFiles.contains(file)) {
+        removedFiles.add(file);
+      }
+    }
+    ImmutableMap.Builder<File, FileEntry> fileMap = ImmutableMap.builder();
+    for (FileEntry fileEntry : fileEntryList) {
+      fileMap.put(fileEntry.file, fileEntry);
+    }
+    State newState = new State();
+    newState.fileEntryMap = fileMap.build();
+    return newState;
+  }
+
+  private static List<FileEntry> updateTimeStamps(@NotNull Iterable<File> fileList) throws Exception {
+    final FileAttributeProvider fileAttributeProvider = FileAttributeProvider.getInstance();
+    List<ListenableFuture<FileEntry>> futures = Lists.newArrayList();
+    for (File file : fileList) {
+      futures.add(submit(() -> {
+                           FileEntry fileEntry = new FileEntry();
+                           fileEntry.file = file;
+                           fileEntry.timestamp = fileAttributeProvider.getFileModifiedTime(fileEntry.file);
+                           return fileEntry;
+                         }
+      ));
+    }
+    return Futures.allAsList(futures).get();
+  }
+
+  private static <T> ListenableFuture<T> submit(Callable<T> callable) {
+    return BlazeExecutor.getInstance().submit(callable);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
new file mode 100644
index 0000000..5fe25e7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ContentEntryEditor.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectstructure;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.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.projectview.ImportRoots;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ContentEntry;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.util.io.URLUtil;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+
+public class ContentEntryEditor {
+
+  public static void createContentEntries(Project project,
+                                          BlazeContext context,
+                                          WorkspaceRoot workspaceRoot,
+                                          ProjectViewSet projectViewSet,
+                                          BlazeProjectData blazeProjectData,
+                                          ModifiableRootModel modifiableRootModel) {
+    ImportRoots importRoots = ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project))
+      .add(projectViewSet)
+      .build();
+    Collection<WorkspacePath> rootDirectories = importRoots.rootDirectories();
+    Collection<WorkspacePath> excludeDirectories = importRoots.excludeDirectories();
+    Multimap<WorkspacePath, WorkspacePath> excludesByRootDirectory = sortExcludesByRootDirectory(rootDirectories, excludeDirectories);
+
+
+    List<ContentEntry> contentEntries = Lists.newArrayList();
+    for (WorkspacePath rootDirectory : rootDirectories) {
+      File root = workspaceRoot.fileForPath(rootDirectory);
+      ContentEntry contentEntry = modifiableRootModel.addContentEntry(pathToUrl(root.getPath()));
+      contentEntries.add(contentEntry);
+
+      for (WorkspacePath exclude : excludesByRootDirectory.get(rootDirectory)) {
+        File excludeFolder = workspaceRoot.fileForPath(exclude);
+        contentEntry.addExcludeFolder(pathToIdeaUrl(excludeFolder));
+      }
+    }
+
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      syncPlugin.updateContentEntries(
+        project,
+        context,
+        workspaceRoot,
+        projectViewSet,
+        blazeProjectData,
+        contentEntries
+      );
+    }
+  }
+
+  private static Multimap<WorkspacePath, WorkspacePath> sortExcludesByRootDirectory(
+    Collection<WorkspacePath> rootDirectories,
+    Collection<WorkspacePath> excludedDirectories) {
+
+    Multimap<WorkspacePath, WorkspacePath> result = ArrayListMultimap.create();
+    for (WorkspacePath exclude : excludedDirectories) {
+      WorkspacePath foundWorkspacePath = rootDirectories
+        .stream()
+        .filter(rootDirectory -> isUnderRootDirectory(rootDirectory, exclude.relativePath()))
+        .findFirst()
+        .orElse(null);
+      if (foundWorkspacePath != null) {
+        result.put(foundWorkspacePath, exclude);
+      }
+    }
+    return result;
+  }
+
+  private static boolean isUnderRootDirectory(WorkspacePath rootDirectory, String relativePath) {
+    if (rootDirectory.isWorkspaceRoot()) {
+      return true;
+    }
+    String rootDirectoryString = rootDirectory.toString();
+    return relativePath.startsWith(rootDirectoryString)
+           && (relativePath.length() == rootDirectoryString.length()
+               || (relativePath.charAt(rootDirectoryString.length()) == '/'));
+  }
+
+  @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);
+    }
+  }
+
+  @NotNull
+  private static String pathToIdeaUrl(@NotNull File path) {
+    return pathToUrl(toSystemIndependentName(path.getPath()));
+  }
+  @NotNull
+  private static String toSystemIndependentName(@NonNls @NotNull String aFileName) {
+    return FileUtilRt.toSystemIndependentName(aFileName);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleDataStorage.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleDataStorage.java
new file mode 100644
index 0000000..9371728
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleDataStorage.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectstructure;
+
+/**
+ * Constants about where we store module data.
+ */
+public class ModuleDataStorage {
+  public static final String MODULE_DATA_SUBDIRECTORY = "modules";
+  public static final String DATA_SUBDIRECTORY = ".blaze";
+  public static final String WORKSPACE_MODULE_NAME = ".workspace";
+  public static final String PROJECT_DATA_DIR_MODULE_NAME = ".project-data-dir";
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorImpl.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorImpl.java
new file mode 100644
index 0000000..2ae1167
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorImpl.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.base.sync.projectstructure;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.intellij.ide.highlighter.ModuleFileType;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.ModifiableModuleModel;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleManager;
+import com.intellij.openapi.module.ModuleType;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.CompilerModuleExtension;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.ModuleRootManager;
+import com.intellij.openapi.roots.impl.ModifiableModelCommitter;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Module editor implementation.
+ */
+public class ModuleEditorImpl implements BlazeSyncPlugin.ModuleEditor {
+  private static final Logger LOG = Logger.getInstance(ModuleEditorImpl.class.getName());
+  private static final String EXTERNAL_SYSTEM_ID_KEY = "external.system.id";
+  private static final String EXTERNAL_SYSTEM_ID_VALUE = "Blaze";
+
+  private final Project project;
+  private final ModifiableModuleModel moduleModel;
+  private final File imlDirectory;
+  private final Set<String> moduleNames = Sets.newHashSet();
+  @VisibleForTesting
+  public Collection<ModifiableRootModel> modifiableModels = Lists.newArrayList();
+
+  public ModuleEditorImpl(Project project, BlazeImportSettings importSettings) {
+    this.project = project;
+    this.moduleModel = ModuleManager.getInstance(project).getModifiableModel();
+
+    this.imlDirectory = getImlDirectory(importSettings);
+    if (!FileAttributeProvider.getInstance().exists(imlDirectory)) {
+      if (!imlDirectory.mkdirs()) {
+        LOG.error("Could not make directory: " + imlDirectory.getPath());
+      }
+    }
+  }
+
+  @Override
+  public boolean registerModule(String moduleName) {
+    boolean hasModule = moduleModel.findModuleByName(moduleName) != null;
+    if (hasModule) {
+      moduleNames.add(moduleName);
+    }
+    return hasModule;
+  }
+
+  @Override
+  public Module createModule(String moduleName, ModuleType moduleType) {
+    Module module = moduleModel.findModuleByName(moduleName);
+    if (module == null) {
+      File imlFile = new File(imlDirectory, moduleName + ModuleFileType.DOT_DEFAULT_EXTENSION);
+      removeImlFile(imlFile);
+      module = moduleModel.newModule(imlFile.getPath(), moduleType.getId());
+      module.setOption(EXTERNAL_SYSTEM_ID_KEY, EXTERNAL_SYSTEM_ID_VALUE);
+    }
+    module.setOption(Module.ELEMENT_TYPE, moduleType.getId());
+    moduleNames.add(moduleName);
+    return module;
+  }
+
+  @Override
+  public ModifiableRootModel editModule(Module module) {
+    ModifiableRootModel modifiableModel = ModuleRootManager.getInstance(module).getModifiableModel();
+    modifiableModels.add(modifiableModel);
+
+    modifiableModel.clear();
+    modifiableModel.inheritSdk();
+    CompilerModuleExtension compilerSettings = modifiableModel.getModuleExtension(CompilerModuleExtension.class);
+    if (compilerSettings != null) {
+      compilerSettings.inheritCompilerOutputPath(false);
+    }
+
+    return modifiableModel;
+  }
+
+  @Override
+  @Nullable
+  public Module findModule(String moduleName) {
+    return moduleModel.findModuleByName(moduleName);
+  }
+
+  public void commitWithGc(BlazeContext context) {
+    List<Module> orphanModules = Lists.newArrayList();
+    for (Module module : ModuleManager.getInstance(project).getModules()) {
+      if (!moduleNames.contains(module.getName())) {
+        orphanModules.add(module);
+      }
+    }
+    if (orphanModules.size() > 0) {
+      context.output(new PrintOutput(
+        String.format("Removing %d dead modules", orphanModules.size()))
+      );
+      for (Module module : orphanModules) {
+        if (module.isDisposed()) {
+          continue;
+        }
+        moduleModel.disposeModule(module);
+        File imlFile = new File(module.getModuleFilePath());
+        removeImlFile(imlFile);
+      }
+    }
+
+    context.output(new PrintOutput(
+      String.format("Workspace has %s modules", modifiableModels.size())
+    ));
+
+    commit();
+  }
+
+  @Override
+  public void commit() {
+    ModifiableModelCommitter.multiCommit(modifiableModels, moduleModel);
+  }
+
+  private File getImlDirectory(BlazeImportSettings importSettings) {
+    return new File(
+      new File(importSettings.getProjectDataDirectory(), ModuleDataStorage.DATA_SUBDIRECTORY),
+      ModuleDataStorage.MODULE_DATA_SUBDIRECTORY
+    );
+  }
+
+  // Delete using the virtual file to ensure that IntelliJ properly updates its index. Otherwise, it is possible for IntelliJ to read the
+  // old IML file from its index and behave unpredictably (like failing to save the new IML files to disk).
+  private static void removeImlFile(final File imlFile) {
+    final VirtualFile imlVirtualFile = VfsUtil.findFileByIoFile(imlFile, true);
+    if (imlVirtualFile != null && imlVirtualFile.exists()) {
+      ApplicationManager.getApplication().runWriteAction(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            imlVirtualFile.delete(this);
+          }
+          catch (IOException e) {
+            LOG.warn(String.format("Could not delete file: %s, will try to continue anyway.", imlVirtualFile.getPath()), e);
+          }
+        }
+      });
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorProvider.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorProvider.java
new file mode 100644
index 0000000..3c00fe4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorProvider.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.sync.projectstructure;
+
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Provides a ModuleEditor. This indirection is required to avoid committing modules
+ * during integration tests of the sync process, as this is not allowed by LightPlatformTestCase.
+ */
+public interface ModuleEditorProvider {
+
+  static ModuleEditorProvider getInstance() {
+    return ServiceManager.getService(ModuleEditorProvider.class);
+  }
+
+  ModuleEditorImpl getModuleEditor(Project project, BlazeImportSettings importSettings);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorProviderImpl.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorProviderImpl.java
new file mode 100644
index 0000000..dfc5fe7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectstructure/ModuleEditorProviderImpl.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectstructure;
+
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Provides a ModuleEditor. This indirection is required to avoid committing modules
+ * during integration tests of the sync process, as this is not allowed by LightPlatformTestCase.
+ */
+public class ModuleEditorProviderImpl implements ModuleEditorProvider {
+
+  @Override
+  public ModuleEditorImpl getModuleEditor(Project project, BlazeImportSettings importSettings) {
+    return new ModuleEditorImpl(project, importSettings);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectview/ImportRoots.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/ImportRoots.java
new file mode 100644
index 0000000..4e7ef66
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/ImportRoots.java
@@ -0,0 +1,145 @@
+/*
+ * 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 com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * The roots to import. Derived from project view.
+ */
+public final class ImportRoots {
+  public static class Builder {
+    private final ImmutableCollection.Builder<WorkspacePath> rootDirectoriesBuilder = ImmutableList.builder();
+    private final ImmutableSet.Builder<WorkspacePath> excludeDirectoriesBuilder = ImmutableSet.builder();
+
+    private final WorkspaceRoot workspaceRoot;
+    private final BuildSystem buildSystem;
+
+    private Builder(WorkspaceRoot workspaceRoot, BuildSystem buildSystem) {
+      this.workspaceRoot = workspaceRoot;
+      this.buildSystem = buildSystem;
+    }
+
+    public Builder add(ProjectViewSet projectViewSet) {
+      for (DirectoryEntry entry : projectViewSet.listItems(DirectorySection.KEY)) {
+        add(entry);
+      }
+      return this;
+    }
+
+    @VisibleForTesting
+    public Builder add(DirectoryEntry entry) {
+      if (entry.included) {
+        rootDirectoriesBuilder.add(entry.directory);
+      } else {
+        excludeDirectoriesBuilder.add(entry.directory);
+      }
+      return this;
+    }
+
+    public ImportRoots build() {
+      ImmutableCollection<WorkspacePath> rootDirectories = rootDirectoriesBuilder.build();
+      // for bazel projects, if we're including the workspace root, we force-exclude the bazel artifact directories
+      // (e.g. bazel-bin, bazel-genfiles).
+      if (buildSystem == BuildSystem.Bazel && hasWorkspaceRoot(rootDirectories)) {
+        excludeBuildSystemArtifacts();
+      }
+      return new ImportRoots(rootDirectories, excludeDirectoriesBuilder.build());
+    }
+
+    private void excludeBuildSystemArtifacts() {
+      for (String dir : BuildSystemProvider.getBuildSystemProvider(buildSystem).buildArtifactDirectories(workspaceRoot)) {
+        excludeDirectoriesBuilder.add(new WorkspacePath(dir));
+      }
+    }
+
+    private static boolean hasWorkspaceRoot(ImmutableCollection<WorkspacePath> rootDirectories) {
+      return rootDirectories.stream().anyMatch(WorkspacePath::isWorkspaceRoot);
+    }
+
+  }
+
+  private final ImmutableCollection<WorkspacePath> rootDirectories;
+  private final ImmutableSet<WorkspacePath> excludeDirectories;
+
+  public static Builder builder(WorkspaceRoot workspaceRoot, BuildSystem buildSystem) {
+    return new Builder(workspaceRoot, buildSystem);
+  }
+
+  private ImportRoots(
+    ImmutableCollection<WorkspacePath> rootDirectories,
+    ImmutableSet<WorkspacePath> excludeDirectories) {
+    this.rootDirectories = rootDirectories;
+    this.excludeDirectories = excludeDirectories;
+  }
+
+  public Collection<WorkspacePath> rootDirectories() {
+    return rootDirectories;
+  }
+
+  public Set<WorkspacePath> excludeDirectories() {
+    return excludeDirectories;
+  }
+
+  /**
+   * Returns true if this rule should be imported as source.
+   */
+  public boolean importAsSource(Label label) {
+    return containsLabel(label);
+  }
+
+  private boolean containsLabel(Label label) {
+    boolean included = false;
+    boolean excluded = false;
+    for (WorkspacePath workspacePath : rootDirectories()) {
+      included = included || matchesLabel(workspacePath, label);
+    }
+    for (WorkspacePath workspacePath : excludeDirectories()) {
+      excluded = excluded || matchesLabel(workspacePath, label);
+    }
+    return included && !excluded;
+  }
+
+  private static boolean matchesLabel(WorkspacePath workspacePath, Label label) {
+    if (workspacePath.isWorkspaceRoot()) {
+      return true;
+    }
+    String moduleLabelStr = label.toString();
+    int packagePrefixLength = "//".length();
+    int nextCharIndex = workspacePath.relativePath().length() + packagePrefixLength;
+    if (moduleLabelStr.startsWith(workspacePath.relativePath(), packagePrefixLength)
+        && moduleLabelStr.length() >= nextCharIndex) {
+      char c = moduleLabelStr.charAt(nextCharIndex);
+      return c == '/' || c == ':';
+    }
+    return false;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
new file mode 100644
index 0000000..e3a8d97
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/LanguageSupport.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.AdditionalLanguagesSection;
+import com.google.idea.blaze.base.projectview.section.sections.WorkspaceTypeSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.intellij.openapi.diagnostic.Logger;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Reads the user's language preferences from the project view.
+ */
+public class LanguageSupport {
+
+  private static final Logger LOG = Logger.getInstance(LanguageSupport.class);
+
+  public static WorkspaceLanguageSettings createWorkspaceLanguageSettings(BlazeContext context, ProjectViewSet projectViewSet) {
+    WorkspaceType workspaceType = projectViewSet.getSectionValue(WorkspaceTypeSection.KEY);
+    if (workspaceType == null) {
+      for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+        WorkspaceType pluginWorkspaceType = syncPlugin.getDefaultWorkspaceType();
+        if (pluginWorkspaceType != null) {
+          if (workspaceType == null || workspaceType.ordinal() < pluginWorkspaceType.ordinal()) {
+            workspaceType = pluginWorkspaceType;
+          }
+        }
+      }
+    }
+
+    if (workspaceType == null) {
+      LOG.error("Could not find workspace type."); // Should never happen
+      return null;
+    }
+
+    Set<LanguageClass> activeLanguages = Sets.newHashSet();
+    for (LanguageClass languageClass : workspaceType.getLanguages()) {
+      activeLanguages.add(languageClass);
+    }
+    activeLanguages.addAll(projectViewSet.listItems(AdditionalLanguagesSection.KEY));
+
+    Set<LanguageClass> supportedLanguages = Sets.newHashSet();
+    for (BlazeSyncPlugin syncPlugin : BlazeSyncPlugin.EP_NAME.getExtensions()) {
+      supportedLanguages.addAll(syncPlugin.getSupportedLanguagesInWorkspace(workspaceType));
+    }
+
+    for (LanguageClass languageClass : activeLanguages) {
+      if (!supportedLanguages.contains(languageClass)) {
+        IssueOutput
+          .error(String.format(
+            "Language '%s' is not supported for this plugin with workspace type: '%s'",
+            languageClass.getName(), workspaceType.getName()))
+          .submit(context);
+        return null;
+      }
+    }
+
+    return new WorkspaceLanguageSettings(workspaceType, activeLanguages);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectview/ProjectViewRuleImportFilter.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/ProjectViewRuleImportFilter.java
new file mode 100644
index 0000000..1585de9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/ProjectViewRuleImportFilter.java
@@ -0,0 +1,58 @@
+/*
+ * 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 com.google.common.collect.Sets;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.Tags;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.sections.ExcludeTargetSection;
+import com.google.idea.blaze.base.projectview.section.sections.ImportTargetOutputSection;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.Project;
+
+import java.util.Set;
+
+/**
+ * Filters rules into source/library depending on the project view.
+ */
+public class ProjectViewRuleImportFilter {
+  private final ImportRoots importRoots;
+  private final Set<Label> importTargetOutputs;
+  private final Set<Label> excludedTargets;
+
+  public ProjectViewRuleImportFilter(Project project, WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet) {
+    this.importRoots = ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project)).add(projectViewSet).build();
+    this.importTargetOutputs = Sets.newHashSet(projectViewSet.listItems(ImportTargetOutputSection.KEY));
+    this.excludedTargets = Sets.newHashSet(projectViewSet.listItems(ExcludeTargetSection.KEY));
+  }
+
+  public boolean isSourceRule(RuleIdeInfo rule) {
+    return importRoots.importAsSource(rule.label) && !importTargetOutput(rule);
+  }
+
+  private boolean importTargetOutput(RuleIdeInfo rule) {
+    return rule.tags.contains(Tags.RULE_TAG_IMPORT_TARGET_OUTPUT)
+           || rule.tags.contains(Tags.RULE_TAG_IMPORT_AS_LIBRARY_LEGACY)
+           || importTargetOutputs.contains(rule.label);
+  }
+
+  public boolean excludeTarget(RuleIdeInfo rule) {
+    return excludedTargets.contains(rule.label) || rule.tags.contains(Tags.RULE_TAG_PROVIDED_BY_SDK);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectview/SourceTestConfig.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/SourceTestConfig.java
new file mode 100644
index 0000000..04ab82c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/SourceTestConfig.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.projectview;
+
+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;
+
+/**
+ * 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));
+  }
+
+  /**
+   * Returns true if this artifact is a test artifact.
+   */
+  public boolean isTestSource(String relativePath) {
+    return testSources.matches(relativePath);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java
new file mode 100644
index 0000000..3c9e5f2
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/projectview/WorkspaceLanguageSettings.java
@@ -0,0 +1,84 @@
+/*
+ * 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 com.google.common.base.Objects;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.Serializable;
+import java.util.Set;
+
+/**
+ * Contains the user's language preferences from the project view.
+ */
+@Immutable
+public class WorkspaceLanguageSettings implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  private final WorkspaceType workspaceType;
+  private final Set<LanguageClass> activeLanguages;
+
+  public WorkspaceLanguageSettings(WorkspaceType workspaceType,
+                                   Set<LanguageClass> activeLanguages) {
+    this.workspaceType = workspaceType;
+    this.activeLanguages = activeLanguages;
+  }
+
+  public WorkspaceType getWorkspaceType() {
+    return workspaceType;
+  }
+
+  public boolean isWorkspaceType(WorkspaceType workspaceType) {
+    return this.workspaceType == workspaceType;
+  }
+
+  public boolean isWorkspaceType(WorkspaceType... workspaceTypes) {
+    for (WorkspaceType workspaceType : workspaceTypes) {
+      if (this.workspaceType == workspaceType) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public boolean isLanguageActive(LanguageClass languageClass) {
+    return activeLanguages.contains(languageClass);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    WorkspaceLanguageSettings that = (WorkspaceLanguageSettings)o;
+    return workspaceType == that.workspaceType
+           && Objects.equal(activeLanguages, that.activeLanguages);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(workspaceType, activeLanguages);
+  }
+
+  @Override
+  public String toString() {
+    return "WorkspaceLanguageSettings {" + "\n"
+           + "  workspaceType: " + workspaceType + "\n"
+           + "  activeLanguages: " + activeLanguages + "\n"
+           + '}';
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/sdk/DefaultSdkProvider.java b/blaze-base/src/com/google/idea/blaze/base/sync/sdk/DefaultSdkProvider.java
new file mode 100644
index 0000000..e444984
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/sdk/DefaultSdkProvider.java
@@ -0,0 +1,32 @@
+/*
+ * 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.sdk;
+
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.intellij.openapi.extensions.ExtensionPointName;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * May download or otherwise provide default sdk locations for languages.
+ */
+public interface DefaultSdkProvider {
+  ExtensionPointName<DefaultSdkProvider> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.DefaultSdkProvider");
+
+  @Nullable
+  File provideSdkForLanguage(LanguageClass language);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatus.java b/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatus.java
new file mode 100644
index 0000000..1bc7381
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatus.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.status;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Interface to tell blaze it might need to resync.
+ */
+public interface BlazeSyncStatus {
+
+  enum SyncStatus {
+    FAILED,
+    DIRTY,
+    CLEAN,
+  }
+
+  SyncStatus getStatus();
+
+  static BlazeSyncStatus getInstance(Project project) {
+    return ServiceManager.getService(project, BlazeSyncStatus.class);
+  }
+
+  void setDirty();
+
+  void queueAutomaticSyncIfDirty();
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java b/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
new file mode 100644
index 0000000..dba518e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusImpl.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.status;
+
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.actions.IncrementalSyncProjectAction;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.fileEditor.*;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.*;
+import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Per-project listener for changes to BUILD files, and other changes requiring an incremental sync.
+ */
+public class BlazeSyncStatusImpl implements BlazeSyncStatus {
+
+  public static final BoolExperiment AUTOMATIC_INCREMENTAL_SYNC =
+    new BoolExperiment("automatic.incremental.sync", true);
+
+  public static BlazeSyncStatusImpl getImpl(@NotNull Project project) {
+    return (BlazeSyncStatusImpl) BlazeSyncStatus.getInstance(project);
+  }
+
+  private static Logger log = Logger.getInstance(BlazeSyncStatusImpl.class);
+
+  private final Project project;
+
+  public final AtomicBoolean syncInProgress = new AtomicBoolean(false);
+  private final AtomicBoolean syncPending = new AtomicBoolean(false);
+
+  /**
+   * has a BUILD file changed since the last sync started
+   */
+  private volatile boolean dirty = false;
+
+  private volatile boolean failedSync = false;
+
+  public BlazeSyncStatusImpl(Project project) {
+    this.project = project;
+    // listen for changes to the VFS
+    VirtualFileManager.getInstance().addVirtualFileListener(new FileListener(), project);
+
+    // trigger VFS updates whenever navigating away from an unsaved BUILD file
+    project.getMessageBus().connect().subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER,
+                                                new FileFocusListener());
+  }
+
+  private static boolean automaticSyncEnabled() {
+    return AUTOMATIC_INCREMENTAL_SYNC.getValue()
+        && BlazeUserSettings.getInstance().getResyncAutomatically();
+  }
+
+  @Override
+  public SyncStatus getStatus() {
+    if (failedSync) {
+      return SyncStatus.FAILED;
+    }
+    return dirty ? SyncStatus.DIRTY : SyncStatus.CLEAN;
+  }
+
+  public void syncStarted() {
+    syncPending.set(false);
+    syncInProgress.set(true);
+  }
+
+  public void syncEnded(boolean successful) {
+    syncInProgress.set(false);
+    failedSync = !successful;
+    if (successful && !syncPending.get()) {
+      dirty = false;
+    }
+  }
+
+  @Override
+  public void setDirty() {
+    dirty = true;
+    queueIncrementalSync();
+  }
+
+  @Override
+  public void queueAutomaticSyncIfDirty() {
+    if (dirty) {
+      queueIncrementalSync();
+    }
+  }
+
+  private void queueIncrementalSync() {
+    if (automaticSyncEnabled() && syncPending.compareAndSet(false, true)) {
+      log.info("Automatic sync started");
+      BlazeSyncManager.getInstance(project).requestProjectSync(IncrementalSyncProjectAction.autoSyncParams);
+    }
+  }
+
+  /**
+   * Listens for changes to files which impact the sync process
+   * (BUILD files and project view files)
+   */
+  private class FileListener extends VirtualFileAdapter {
+    @Override
+    public void fileCreated(@NotNull VirtualFileEvent event) {
+      processEvent(event);
+    }
+
+    @Override
+    public void fileDeleted(@NotNull VirtualFileEvent event){
+      processEvent(event);
+      // we (sometimes) only get one event when a directory is deleted, so check the children too.
+      checkChildren(event.getFile());
+    }
+
+    @Override
+    public void fileMoved(@NotNull VirtualFileMoveEvent event){
+      processEvent(event);
+    }
+
+    @Override
+    public void contentsChanged(@NotNull VirtualFileEvent event){
+      processEvent(event);
+    }
+
+    private void processEvent(@NotNull VirtualFileEvent event) {
+      if (isSyncSensitiveFile(event.getFile())) {
+        setDirty();
+      }
+    }
+
+    private void checkChildren(VirtualFile file) {
+      if (!(file instanceof NewVirtualFile)) {
+        return;
+      }
+      Collection<VirtualFile> children = ((NewVirtualFile) file).getCachedChildren();
+      for (VirtualFile child : children) {
+        if (isSyncSensitiveFile(child)) {
+          setDirty();
+          return;
+        }
+      }
+    }
+  }
+
+  /**
+   * Listens for changes to files which impact the sync process
+   * (BUILD files and project view files)
+   */
+  private static class FileFocusListener extends FileEditorManagerAdapter {
+    @Override
+    public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) {
+      processEvent(file);
+    }
+
+    @Override
+    public void selectionChanged(@NotNull FileEditorManagerEvent event) {
+      processEvent(event.getOldFile());
+    }
+
+    private void processEvent(@Nullable VirtualFile file) {
+      if (isSyncSensitiveFile(file)) {
+        FileDocumentManager manager = FileDocumentManager.getInstance();
+        Document doc = manager.getCachedDocument(file);
+        if (doc != null) {
+          manager.saveDocument(doc);
+        }
+      }
+    }
+  }
+
+  private static boolean isSyncSensitiveFile(@Nullable VirtualFile file) {
+    return file != null && (isBuildFile(file) || ProjectViewStorageManager.isProjectViewFile(file.getPath()));
+  }
+
+
+  private static boolean isBuildFile(VirtualFile file) {
+    return file.getName().equals("BUILD");
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusListener.java b/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusListener.java
new file mode 100644
index 0000000..382e022
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/status/BlazeSyncStatusListener.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.status;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.sync.SyncListener;
+import com.intellij.openapi.project.Project;
+
+/**
+ * Application-wide listener for blaze syncs. Notifies per-project status listener when
+ * they start and finish.
+ */
+public class BlazeSyncStatusListener implements SyncListener {
+
+  @Override
+  public void onSyncStart(Project project) {
+    BlazeSyncStatusImpl.getImpl(project).syncStarted();
+  }
+
+  @Override
+  public void afterSync(Project project,
+                        boolean successful) {
+    BlazeSyncStatusImpl.getImpl(project).syncEnded(successful);
+  }
+
+  @Override
+  public void onSyncComplete(
+    Project project,
+    BlazeImportSettings importSettings,
+    ProjectViewSet projectViewSet,
+    BlazeProjectData blazeProjectData) {
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoder.java b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoder.java
new file mode 100644
index 0000000..19223da
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoder.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * Decodes android_studio_ide_info.proto ArtifactLocation file paths
+ */
+public class ArtifactLocationDecoder {
+
+  private final BlazeRoots blazeRoots;
+  private final WorkspacePathResolver pathResolver;
+
+  public ArtifactLocationDecoder(BlazeRoots blazeRoots, WorkspacePathResolver pathResolver) {
+    this.blazeRoots = blazeRoots;
+    this.pathResolver = pathResolver;
+  }
+
+  /**
+   * Decodes the ArtifactLocation proto, locates the absolute artifact file path.
+   * Returns null if the file can't be found (presumably because it was removed
+   * since the blaze build)
+   */
+  @Nullable
+  public ArtifactLocation decode(AndroidStudioIdeInfo.ArtifactLocation loc) {
+    return decode(loc.getRootPath(),
+                  loc.getRootExecutionPathFragment(),
+                  loc.getRelativePath(),
+                  loc.getIsSource());
+  }
+
+  /**
+   * Decodes the ArtifactLocation proto, locates the absolute artifact file path.
+   * Returns null if the file can't be found (presumably because it was removed
+   * since the blaze build)
+   */
+  @Nullable
+  public ArtifactLocation decode(PackageManifestOuterClass.ArtifactLocation loc) {
+    return decode(loc.getRootPath(),
+                  loc.getRootExecutionPathFragment(),
+                  loc.getRelativePath(),
+                  loc.getIsSource());
+  }
+
+  @Nullable
+  private ArtifactLocation decode(
+    String rootPath,
+    String rootExecutionPathFragment,
+    String relativePath,
+    boolean isSource) {
+    File root;
+    if (isSource) {
+      root = pathResolver.findPackageRoot(relativePath);
+    } else {
+      if (rootExecutionPathFragment.isEmpty()) {
+        // old format -- derive execution path fragment from the root path.
+        // it's a backwards way of doing it -- but we want to test the new code,
+        // and this will soon be removed
+        rootExecutionPathFragment = deriveRootExecutionPathFragmentFromRoot(rootPath);
+      }
+      root = new File(blazeRoots.executionRoot, rootExecutionPathFragment);
+    }
+    if (root == null) {
+      return null;
+    }
+    return ArtifactLocation.builder()
+      .setRootPath(root.toString())
+      .setRootExecutionPathFragment(rootExecutionPathFragment)
+      .setRelativePath(relativePath)
+      .setIsSource(isSource)
+      .build();
+  }
+
+  @Deprecated
+  private String deriveRootExecutionPathFragmentFromRoot(String rootPath) {
+    String execRoot = blazeRoots.executionRoot.toString();
+    if (rootPath.startsWith(execRoot)) {
+      return rootPath.substring(execRoot.length());
+    }
+    return "";
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
new file mode 100644
index 0000000..65b6145
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/BlazeRoots.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * The data output by BlazeInfo.
+ */
+public class BlazeRoots implements Serializable {
+  public static final long serialVersionUID = 3L;
+  private static final Logger LOG = Logger.getInstance(BlazeRoots.class);
+
+  public static ListenableFuture<BlazeRoots> compute(Project project, WorkspaceRoot workspaceRoot, BlazeContext context) {
+    BuildSystem buildSystem = Blaze.getBuildSystem(project);
+    ListenableFuture<ImmutableMap<String, String>> blazeInfoDataFuture =
+      BlazeInfo.getInstance().runBlazeInfo(context, buildSystem, workspaceRoot, ImmutableList.of());
+    return Futures.transform(
+      blazeInfoDataFuture,
+      new Function<ImmutableMap<String, String>, BlazeRoots>() {
+        @Nullable
+        @Override
+        public BlazeRoots apply(@Nullable ImmutableMap<String, String> blazeInfoData) {
+          // This method is supposed to throw if the input is null but the input is not allowed to be null.
+          if (blazeInfoData == null) {
+            throw new NullPointerException("blazeInfoData is not allowed to be null");
+          }
+          return build(
+            workspaceRoot,
+            getOrThrow(buildSystem, blazeInfoData, BlazeInfo.EXECUTION_ROOT_KEY),
+            getOrThrow(buildSystem, blazeInfoData, BlazeInfo.PACKAGE_PATH_KEY),
+            getOrThrow(buildSystem, blazeInfoData, BlazeInfo.blazeBinKey(buildSystem)),
+            getOrThrow(buildSystem, blazeInfoData, BlazeInfo.blazeGenfilesKey(buildSystem))
+          );
+        }
+      }
+    );
+  }
+
+  private static String getOrThrow(BuildSystem buildSystem, ImmutableMap<String, String> map, String key) {
+    String value = map.get(key);
+    if (value == null) {
+      throw new RuntimeException(String.format("Could not locate %s in %s info", key, buildSystem.getLowerCaseName()));
+    }
+    return value;
+  }
+
+  private static BlazeRoots build(
+    WorkspaceRoot workspaceRoot,
+    String execRootString,
+    String packagePathString,
+    String blazeBinRoot,
+    String blazeGenfilesRoot
+  ) {
+    List<File> packagePaths = parsePackagePaths(workspaceRoot.toString(), packagePathString.trim());
+    File executionRoot = new File(execRootString.trim());
+    ExecutionRootPath blazeBinExecutionRootPath = ExecutionRootPath.createAncestorRelativePath(
+      executionRoot,
+      new File(blazeBinRoot)
+    );
+    ExecutionRootPath blazeGenfilesExecutionRootPath = ExecutionRootPath.createAncestorRelativePath(
+      executionRoot,
+      new File(blazeGenfilesRoot)
+    );
+    LOG.assertTrue(blazeBinExecutionRootPath != null);
+    LOG.assertTrue(blazeGenfilesExecutionRootPath != null);
+    return new BlazeRoots(executionRoot, packagePaths, blazeBinExecutionRootPath, blazeGenfilesExecutionRootPath);
+  }
+
+  private static List<File> parsePackagePaths(String workspaceRoot,
+                                              String packagePathString) {
+    String[] paths = packagePathString.split(":");
+    List<File> packagePaths = Lists.newArrayListWithCapacity(paths.length);
+    FileAttributeProvider fileAttributeProvider = FileAttributeProvider.getInstance();
+    for (String path : paths) {
+      File packagePath = new File(path.replace("%workspace%", workspaceRoot));
+      if (fileAttributeProvider.exists(packagePath)) {
+        packagePaths.add(packagePath);
+      }
+    }
+    return packagePaths;
+  }
+
+  public final File executionRoot;
+  public final List<File> packagePaths;
+  public final ExecutionRootPath blazeBinExecutionRootPath;
+  public final ExecutionRootPath blazeGenfilesExecutionRootPath;
+
+  @VisibleForTesting
+  public BlazeRoots(
+    File executionRoot,
+    List<File> packagePaths,
+    ExecutionRootPath blazeBinExecutionRootPath,
+    ExecutionRootPath blazeGenfilesExecutionRootPath
+  ) {
+    this.executionRoot = executionRoot;
+    this.packagePaths = packagePaths;
+    this.blazeBinExecutionRootPath = blazeBinExecutionRootPath;
+    this.blazeGenfilesExecutionRootPath = blazeGenfilesExecutionRootPath;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkingSet.java b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkingSet.java
new file mode 100644
index 0000000..39b5373
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkingSet.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+
+import java.io.Serializable;
+
+/**
+ * Computes the working set of files of directories from source control.
+ */
+public class WorkingSet implements Serializable {
+  private static final long serialVersionUID = 2L;
+
+  public final ImmutableList<WorkspacePath> addedFiles;
+  public final ImmutableList<WorkspacePath> modifiedFiles;
+  public final ImmutableList<WorkspacePath> deletedFiles;
+
+  public WorkingSet(ImmutableList<WorkspacePath> addedFiles,
+                    ImmutableList<WorkspacePath> modifiedFiles,
+                    ImmutableList<WorkspacePath> deletedFiles) {
+    this.addedFiles = addedFiles;
+    this.modifiedFiles = modifiedFiles;
+    this.deletedFiles = deletedFiles;
+  }
+
+  public boolean isEmpty() {
+    return addedFiles.isEmpty() && modifiedFiles.isEmpty() && deletedFiles.isEmpty();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java
new file mode 100644
index 0000000..acc5ea1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolver.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.Serializable;
+
+/**
+ * Uses workspace root, blaze roots and git5 tracked directory information to
+ * convert workspace-relative paths to absolute files with a minimum of file system calls (typically none).
+ */
+public interface WorkspacePathResolver extends Serializable {
+  /**
+   * Resolves a workspace path to an absolute file.
+   */
+  @Nullable
+  default File resolveToFile(WorkspacePath workspacepath) {
+    return resolveToFile(workspacepath.relativePath());
+  }
+
+  /**
+   * Resolves a workspace relative path to an absolute file.
+   */
+  @Nullable
+  default File resolveToFile(String workspaceRelativePath) {
+    File packageRoot = findPackageRoot(workspaceRelativePath);
+    return packageRoot != null ? new File(packageRoot, workspaceRelativePath) : null;
+  }
+
+  /**
+   * This method should be used for directories. In the case that the directory is tracked, it returns the directory under the workspace
+   * root. If the directory is partially tracked (a sub directory is tracked), then the directory in the workspace and the directory under
+   * READONLY are returned in that order in a list. If the directory is untracked, the path is examined to see if this method should return
+   * a file under the execution root or a file under READONLY.
+   */
+  ImmutableList<File> resolveToIncludeDirectories(ExecutionRootPath executionRootPath);
+
+  /**
+   * Finds the package root directory that a workspace relative path is in.
+   */
+  @Nullable
+  File findPackageRoot(String relativePath);
+
+  WorkspaceRoot getWorkspaceRoot();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
new file mode 100644
index 0000000..119b3aa
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImpl.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.List;
+
+/**
+ * Uses the package path locations to resolve a workspace path.
+ */
+public class WorkspacePathResolverImpl implements WorkspacePathResolver {
+  private static final long serialVersionUID = 2L;
+
+  private final WorkspaceRoot workspaceRoot;
+  private final List<File> packagePaths;
+
+  public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot, BlazeRoots blazeRoots) {
+    this(workspaceRoot, blazeRoots.packagePaths);
+  }
+
+  public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot) {
+    this(workspaceRoot, ImmutableList.of(workspaceRoot.directory()));
+  }
+
+  public WorkspacePathResolverImpl(WorkspaceRoot workspaceRoot, List<File> packagePaths) {
+    this.workspaceRoot = workspaceRoot;
+    this.packagePaths = packagePaths;
+  }
+
+  @Override
+  public ImmutableList<File> resolveToIncludeDirectories(ExecutionRootPath executionRootPath) {
+    File trackedLocation = executionRootPath.getFileRootedAt(workspaceRoot.directory());
+    return ImmutableList.of(trackedLocation);
+  }
+
+  @Override
+  @Nullable
+  public File findPackageRoot(String relativePath) {
+    if (packagePaths.size() == 1) {
+      return packagePaths.get(0);
+    }
+    // fall back to manually checking each one
+    FileAttributeProvider existenceChecker = FileAttributeProvider.getInstance();
+    for (File pkg : packagePaths) {
+      if (existenceChecker.exists(new File(pkg, relativePath))) {
+        return pkg;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public WorkspaceRoot getWorkspaceRoot() {
+    return workspaceRoot;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverProvider.java b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverProvider.java
new file mode 100644
index 0000000..1d9d08f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides a WorkspacePathResolver.
+ */
+public interface WorkspacePathResolverProvider {
+
+  static WorkspacePathResolverProvider getInstance(Project project) {
+    return ServiceManager.getService(project, WorkspacePathResolverProvider.class);
+  }
+
+  /**
+   * Returns a WorkspacePathResolver for this project, or null if it's not a blaze/bazel project.
+   */
+  @Nullable
+  WorkspacePathResolver getPathResolver();
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverProviderImpl.java b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverProviderImpl.java
new file mode 100644
index 0000000..6563ed8
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverProviderImpl.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides a WorkspacePathResolver.
+ */
+public class WorkspacePathResolverProviderImpl implements WorkspacePathResolverProvider {
+
+  private final Project project;
+
+  public WorkspacePathResolverProviderImpl(Project project) {
+    this.project = project;
+  }
+
+  @Nullable
+  @Override
+  public WorkspacePathResolver getPathResolver() {
+    BlazeProjectData projectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    return projectData != null ? projectData.workspacePathResolver : null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/trace/Trace.java b/blaze-base/src/com/google/idea/blaze/base/trace/Trace.java
new file mode 100644
index 0000000..07e4b0b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trace/Trace.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.trace;
+
+import com.google.idea.blaze.base.trickle.*;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Helper methods for tracing.
+ */
+public class Trace {
+  public static <R> Function0<R> trace(@NotNull BlazeContext context, @NotNull String name, @NotNull Function0<R> func) {
+    return () -> {
+      try (TraceContext traceContext = new TraceContext(context, name)) {
+        return func.run();
+      }
+    };
+  }
+
+  public static <A, R> Function1<A, R> trace(@NotNull BlazeContext context, @NotNull String name, @NotNull Function1<A, R> func) {
+    return (a) -> {
+      try (TraceContext traceContext = new TraceContext(context, name)) {
+        return func.run(a);
+      }
+    };
+  }
+
+  public static <A, B, R> Function2<A, B, R> trace(@NotNull BlazeContext context, @NotNull String name, @NotNull Function2<A, B, R> func) {
+    return (a, b) -> {
+      try (TraceContext traceContext = new TraceContext(context, name)) {
+        return func.run(a, b);
+      }
+    };
+  }
+
+  public static <A, B, C, R> Function3<A, B, C, R> trace(@NotNull BlazeContext context, @NotNull String name, @NotNull Function3<A, B, C, R> func) {
+    return (a, b, c) -> {
+      try (TraceContext traceContext = new TraceContext(context, name)) {
+        return func.run(a, b, c);
+      }
+    };
+  }
+
+  public static <A, B, C, D, R> Function4<A, B, C, D, R> trace(@NotNull BlazeContext context, @NotNull String name, @NotNull Function4<A, B, C, D, R> func) {
+    return (a, b, c, d) -> {
+      try (TraceContext traceContext = new TraceContext(context, name)) {
+        return func.run(a, b, c, d);
+      }
+    };
+  }
+
+  public static <A, B, C, D, E, R> Function5<A, B, C, D, E, R> trace(@NotNull BlazeContext context, @NotNull String name, @NotNull Function5<A, B, C, D, E, R> func) {
+    return (a, b, c, d, e) -> {
+      try (TraceContext traceContext = new TraceContext(context, name)) {
+        return func.run(a, b, c, d, e);
+      }
+    };
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/trace/TraceContext.java b/blaze-base/src/com/google/idea/blaze/base/trace/TraceContext.java
new file mode 100644
index 0000000..473f871
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trace/TraceContext.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.trace;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Trace context utility class that can be used with try-with-resource.
+ */
+public class TraceContext implements AutoCloseable {
+  @NotNull BlazeContext context;
+  @NotNull String name;
+
+  public TraceContext(@NotNull BlazeContext context, @NotNull String name) {
+    this.context = context;
+    this.name = name;
+    context.output(new TraceEvent(name, TraceEvent.Type.Begin));
+  }
+
+  @Override
+  public void close() {
+    context.output(new TraceEvent(name, TraceEvent.Type.End));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/trace/TraceEvent.java b/blaze-base/src/com/google/idea/blaze/base/trace/TraceEvent.java
new file mode 100644
index 0000000..08e3395
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trace/TraceEvent.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.trace;
+
+import com.google.idea.blaze.base.scope.Output;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Trace event.
+ */
+public class TraceEvent implements Output {
+  public final String name;
+  public final Type type;
+  public final long nanoTime;
+  public long threadId;
+
+  public enum Type {
+    Begin,
+    End,
+  }
+
+  public TraceEvent(@NotNull String name, @NotNull Type type) {
+    this.name = name;
+    this.type = type;
+    this.nanoTime = System.nanoTime();
+    this.threadId = Thread.currentThread().getId();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/trace/TraceScope.java b/blaze-base/src/com/google/idea/blaze/base/trace/TraceScope.java
new file mode 100644
index 0000000..488f3dd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trace/TraceScope.java
@@ -0,0 +1,90 @@
+/*
+ * 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.trace;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.BlazeScope;
+import com.google.idea.blaze.base.scope.OutputSink;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Tracks trace events and writes trace to ~/blaze-trace.json at end of scope.
+ *
+ * The results can be imported into Chrome using chrome://tracing.
+ */
+public class TraceScope implements BlazeScope, OutputSink<TraceEvent> {
+  private static final Logger LOG = Logger.getInstance(TraceScope.class);
+  private final List<TraceEvent> traceEvents = Collections.synchronizedList(Lists.newArrayList());
+  private long traceStartNanos;
+
+  @Override
+  public void onScopeBegin(@NotNull BlazeContext context) {
+    traceStartNanos = System.nanoTime();
+    context.addOutputSink(TraceEvent.class, this);
+  }
+
+  @Override
+  public void onScopeEnd(@NotNull BlazeContext context) {
+    Collections.sort(traceEvents, (a, b) -> Long.compare(a.nanoTime, b.nanoTime));
+
+    String home = System.getProperty("user.home");
+    File file = new File(home, "blaze-trace.json");
+    try (PrintWriter printWriter = new PrintWriter(file)) {
+      printWriter.println("[");
+
+      for (int i = 0; i < traceEvents.size(); ++i) {
+        TraceEvent traceEvent = traceEvents.get(i);
+        long startTimeNanos = traceEvent.nanoTime - traceStartNanos;
+        long startTimeMicros = startTimeNanos / 1000;
+        printWriter.print(String.format(
+          "{\"name\": \"%s\", \"ts\": %d, \"ph\": \"%s\", \"pid\": %d}",
+          traceEvent.name,
+          startTimeMicros,
+          traceEvent.type == TraceEvent.Type.Begin ? "B" : "E",
+          traceEvent.threadId
+        ));
+
+        // No trailing commas in JSON :(
+        if (i != traceEvents.size() - 1) {
+          printWriter.append(',');
+        }
+        printWriter.append('\n');
+      }
+
+      printWriter.println("]");
+    }
+    catch (FileNotFoundException e) {
+      LOG.error(e);
+    }
+
+    context.output(new PrintOutput("Wrote trace output to: " + file));
+  }
+
+  @Override
+  public Propagation onOutput(@NotNull TraceEvent output) {
+    traceEvents.add(output);
+    return Propagation.Continue;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/treeview/BlazePsiDirectoryNode.java b/blaze-base/src/com/google/idea/blaze/base/treeview/BlazePsiDirectoryNode.java
new file mode 100644
index 0000000..5d94e82
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/treeview/BlazePsiDirectoryNode.java
@@ -0,0 +1,60 @@
+/*
+ * 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.treeview;
+
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.ui.SimpleTextAttributes;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A PsiDirectoryNode that doesn't render module names or source roots.
+ */
+public class BlazePsiDirectoryNode extends PsiDirectoryNode {
+  public BlazePsiDirectoryNode(@NotNull PsiDirectoryNode original) {
+    this(original.getProject(), original.getValue(), original.getSettings());
+  }
+
+  public BlazePsiDirectoryNode(Project project, PsiDirectory directory, ViewSettings settings) {
+    super(project, directory, settings);
+  }
+
+  @Override
+  protected boolean shouldShowModuleName() {
+    return false;
+  }
+
+  @Override
+  protected boolean shouldShowSourcesRoot() {
+    return false;
+  }
+
+  @Override
+  protected void updateImpl(PresentationData data) {
+    super.updateImpl(data);
+    PsiDirectory psiDirectory = getValue();
+    assert psiDirectory != null;
+    String text = psiDirectory.getName();
+
+    data.setPresentableText(text);
+    data.clearText();
+    data.addText(text, SimpleTextAttributes.REGULAR_ATTRIBUTES);
+    data.setLocationString("");
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/treeview/BlazePsiDirectoryRootNode.java b/blaze-base/src/com/google/idea/blaze/base/treeview/BlazePsiDirectoryRootNode.java
new file mode 100644
index 0000000..4a71968
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/treeview/BlazePsiDirectoryRootNode.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.treeview;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.ui.SimpleTextAttributes;
+
+/**
+ * A PsiDirectoryNode that represents a directory root, rendering the
+ * whole directory name from the workspace root.
+ */
+public class BlazePsiDirectoryRootNode extends BlazePsiDirectoryNode {
+  public BlazePsiDirectoryRootNode(Project project, PsiDirectory directory, ViewSettings settings) {
+    super(project, directory, settings);
+  }
+
+  @Override
+  protected void updateImpl(PresentationData data) {
+    super.updateImpl(data);
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(getProject());
+    PsiDirectory psiDirectory = getValue();
+    assert psiDirectory != null;
+    WorkspacePath workspacePath = workspaceRoot.workspacePathFor(psiDirectory.getVirtualFile());
+    String text = workspacePath.relativePath();
+
+    data.setPresentableText(text);
+    data.clearText();
+    data.addText(text, SimpleTextAttributes.REGULAR_ATTRIBUTES);
+    data.setLocationString("");
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java b/blaze-base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java
new file mode 100644
index 0000000..85f8aad
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/treeview/BlazeTreeStructureProvider.java
@@ -0,0 +1,154 @@
+/*
+ * 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.treeview;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.ide.projectView.ProjectViewSettings;
+import com.intellij.ide.projectView.TreeStructureProvider;
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.ide.projectView.impl.nodes.ExternalLibrariesNode;
+import com.intellij.ide.projectView.impl.nodes.ProjectViewProjectNode;
+import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode;
+import com.intellij.ide.util.treeView.AbstractTreeNode;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Modifies the project view:
+ *
+ * - Replaces the root with a single workspace root
+ * - Removes rendering of module names and source roots
+ */
+public class BlazeTreeStructureProvider implements TreeStructureProvider, DumbAware {
+  @NotNull
+  @Override
+  public Collection<AbstractTreeNode> modify(@NotNull AbstractTreeNode parent,
+                                             @NotNull Collection<AbstractTreeNode> children,
+                                             ViewSettings settings) {
+    Project project = parent.getProject();
+    if (project == null || !Blaze.isBlazeProject(project)) {
+      return children;
+    }
+
+    if (parent instanceof ProjectViewProjectNode) {
+      WorkspaceRootNode rootNode = createRootNode(project, settings);
+      if (rootNode == null) {
+        return children;
+      }
+      
+      Collection<AbstractTreeNode> result = Lists.newArrayList();
+      result.add(rootNode);
+      for (AbstractTreeNode treeNode : children) {
+        if (treeNode instanceof ExternalLibrariesNode) {
+          result.add(treeNode);
+        }
+      }
+      return result;
+    }
+    else {
+      List<AbstractTreeNode> result = Lists.newArrayList();
+      for (AbstractTreeNode treeNode : children) {
+        if (treeNode.getClass().equals(PsiDirectoryNode.class)) {
+          result.add(new BlazePsiDirectoryNode((PsiDirectoryNode)treeNode));
+        } else {
+          result.add(treeNode);
+        }
+      }
+      return result;
+    }
+  }
+
+  @Nullable
+  private WorkspaceRootNode createRootNode(@NotNull Project project, @NotNull ViewSettings settings) {
+    BlazeImportSettings importSettings = BlazeImportSettingsManager.getInstance(project)
+      .getImportSettings();
+    if (importSettings != null) {
+      WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+      File fdir = workspaceRoot.directory();
+      VirtualFile vdir = LocalFileSystem.getInstance().findFileByIoFile(fdir);
+      if (vdir != null) {
+        final PsiManager psiManager = PsiManager.getInstance(project);
+        PsiDirectory directory = psiManager.findDirectory(vdir);
+        return new WorkspaceRootNode(project, workspaceRoot, directory, wrapViewSettings(settings));
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public Object getData(Collection<AbstractTreeNode> selected, String dataName) {
+    return null;
+  }
+
+  private ViewSettings wrapViewSettings(@NotNull final ViewSettings original) {
+    return new ProjectViewSettings() {
+      @Override
+      public boolean isShowMembers() {
+        return original.isShowMembers();
+      }
+
+      @Override
+      public boolean isStructureView() {
+        return original.isStructureView();
+      }
+
+      @Override
+      public boolean isShowModules() {
+        return original.isShowModules();
+      }
+
+      @Override
+      public boolean isFlattenPackages() {
+        return false;
+      }
+
+      @Override
+      public boolean isAbbreviatePackageNames() {
+        return original.isAbbreviatePackageNames();
+      }
+
+      @Override
+      public boolean isHideEmptyMiddlePackages() {
+        return false;
+      }
+
+      @Override
+      public boolean isShowLibraryContents() {
+        return original.isShowLibraryContents();
+      }
+
+      @Override
+      public boolean isShowExcludedFiles() {
+        return true;
+      }
+    };
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/treeview/WorkspaceRootNode.java b/blaze-base/src/com/google/idea/blaze/base/treeview/WorkspaceRootNode.java
new file mode 100644
index 0000000..e8ee00f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/treeview/WorkspaceRootNode.java
@@ -0,0 +1,113 @@
+/*
+ * 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.treeview;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.ide.projectView.ViewSettings;
+import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode;
+import com.intellij.ide.util.treeView.AbstractTreeNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiManager;
+import com.intellij.ui.SimpleTextAttributes;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Workspace root node.
+ * <p/>
+ * <p>Customizes rendering of the workspace root node to cut out
+ * the full absolute path of the workspace directory.
+ */
+public class WorkspaceRootNode extends PsiDirectoryNode {
+
+  private static final BoolExperiment COLLAPSE_PROJECT_VIEW = new BoolExperiment("collapse.project.view", true);
+
+  private final WorkspaceRoot workspaceRoot;
+
+  public WorkspaceRootNode(Project project,
+                           WorkspaceRoot workspaceRoot,
+                           PsiDirectory value,
+                           ViewSettings viewSettings) {
+    super(project, value, viewSettings);
+    this.workspaceRoot = workspaceRoot;
+  }
+
+  @Override
+  public Collection<AbstractTreeNode> getChildrenImpl() {
+    if (!COLLAPSE_PROJECT_VIEW.getValue()) {
+      return super.getChildrenImpl();
+    }
+    if (!BlazeUserSettings.getInstance().getCollapseProjectView()) {
+      return super.getChildrenImpl();
+    }
+    Project project = getProject();
+    if (project == null) {
+      return super.getChildrenImpl();
+    }
+    List<AbstractTreeNode> children = Lists.newArrayList();
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet == null) {
+      return super.getChildrenImpl();
+    }
+
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+    ImportRoots importRoots = ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project)).add(projectViewSet).build();
+    if (importRoots.rootDirectories().stream().anyMatch(WorkspacePath::isWorkspaceRoot)) {
+      return super.getChildrenImpl();
+    }
+    for (WorkspacePath workspacePath : importRoots.rootDirectories()) {
+      VirtualFile virtualFile = VfsUtil.findFileByIoFile(workspaceRoot.fileForPath(workspacePath), false);
+      if (virtualFile == null) {
+        continue;
+      }
+      PsiDirectory psiDirectory = PsiManager.getInstance(project).findDirectory(virtualFile);
+      if (psiDirectory == null) {
+        continue;
+      }
+      children.add(new BlazePsiDirectoryRootNode(project, psiDirectory, getSettings()));
+    }
+    if (children.isEmpty()) {
+      return super.getChildrenImpl();
+    }
+    return children;
+  }
+
+  @Override
+  protected void updateImpl(PresentationData data) {
+    super.updateImpl(data);
+    PsiDirectory psiDirectory = getValue();
+    assert psiDirectory != null;
+    String text = psiDirectory.getName();
+
+    data.setPresentableText(text);
+    data.clearText();
+    data.addText(text, SimpleTextAttributes.REGULAR_ATTRIBUTES);
+    data.setLocationString("");
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/trickle/Function0.java b/blaze-base/src/com/google/idea/blaze/base/trickle/Function0.java
new file mode 100644
index 0000000..a45bc3d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trickle/Function0.java
@@ -0,0 +1,24 @@
+/*
+ * 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.trickle;
+
+/**
+ * Function of 0 parameters.
+ */
+public interface Function0<R> {
+  R run();
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/trickle/Function1.java b/blaze-base/src/com/google/idea/blaze/base/trickle/Function1.java
new file mode 100644
index 0000000..67893fd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trickle/Function1.java
@@ -0,0 +1,26 @@
+/*
+ * 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.trickle;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Function of 1 parameters.
+ */
+public interface Function1<A, R> {
+  R run(@NotNull A a);
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/trickle/Function2.java b/blaze-base/src/com/google/idea/blaze/base/trickle/Function2.java
new file mode 100644
index 0000000..bc704a1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trickle/Function2.java
@@ -0,0 +1,26 @@
+/*
+ * 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.trickle;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Function of 2 parameters.
+ */
+public interface Function2<A, B, R> {
+  R run(@NotNull A a, @NotNull B b);
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/trickle/Function3.java b/blaze-base/src/com/google/idea/blaze/base/trickle/Function3.java
new file mode 100644
index 0000000..faaecd6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trickle/Function3.java
@@ -0,0 +1,26 @@
+/*
+ * 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.trickle;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Function of 3 parameters.
+ */
+public interface Function3<A, B, C, R> {
+  R run(@NotNull A a, @NotNull B b, @NotNull C c);
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/trickle/Function4.java b/blaze-base/src/com/google/idea/blaze/base/trickle/Function4.java
new file mode 100644
index 0000000..e75b134
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trickle/Function4.java
@@ -0,0 +1,26 @@
+/*
+ * 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.trickle;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Function of 4 parameters.
+ */
+public interface Function4<A, B, C, D, R> {
+  R run(@NotNull A a, @NotNull B b, @NotNull C c, @NotNull D d);
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/trickle/Function5.java b/blaze-base/src/com/google/idea/blaze/base/trickle/Function5.java
new file mode 100644
index 0000000..1573832
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trickle/Function5.java
@@ -0,0 +1,26 @@
+/*
+ * 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.trickle;
+
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Function of 5 parameters.
+ */
+public interface Function5<A, B, C, D, E, R> {
+  R run(@NotNull A a, @NotNull B b, @NotNull C c, @NotNull D d, @NotNull E e);
+}
+
diff --git a/blaze-base/src/com/google/idea/blaze/base/trickle/Tricklex.java b/blaze-base/src/com/google/idea/blaze/base/trickle/Tricklex.java
new file mode 100644
index 0000000..df7dbf9
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/trickle/Tricklex.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.trickle;
+
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.spotify.trickle.*;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Ties Trickle to an executor to cut down on boilerplate.
+ */
+public class Tricklex {
+  public static <R> ConfigurableGraph<R> call(@NotNull Function0<R> func) {
+    return Trickle.call(func0(func));
+  }
+
+  public static <A, R> Trickle.NeedsParameters1<A, R> call(@NotNull Function1<A, R> func) {
+    return Trickle.call(func1(func));
+  }
+
+  public static <A, B, R> Trickle.NeedsParameters2<A, B, R> call(@NotNull Function2<A, B, R> func) {
+    return Trickle.call(func2(func));
+  }
+
+  public static <A, B, C, R> Trickle.NeedsParameters3<A, B, C, R> call(@NotNull Function3<A, B, C, R> func) {
+    return Trickle.call(func3(func));
+  }
+
+  public static <A, B, C, D, R> Trickle.NeedsParameters4<A, B, C, D, R> call(@NotNull Function4<A, B, C, D, R> func) {
+    return Trickle.call(func4(func));
+  }
+
+  public static <A, B, C, D, E, R> Trickle.NeedsParameters5<A, B, C, D, E, R> call(@NotNull Function5<A, B, C, D, E, R> func) {
+    return Trickle.call(func5(func));
+  }
+
+  private static <R> Func0<R> func0(@NotNull final Function0<R> func) {
+    return () -> BlazeExecutor.getInstance().submit(func::run);
+  }
+
+  private static <A, R> Func1<A, R> func1(@NotNull final Function1<A, R> func) {
+    return a -> BlazeExecutor.getInstance().submit(() -> func.run(a));
+  }
+
+  private static <A, B, R> Func2<A, B, R> func2(@NotNull final Function2<A, B, R> func) {
+    return (a, b) -> BlazeExecutor.getInstance().submit(() -> func.run(a, b));
+  }
+
+  private static <A, B, C, R> Func3<A, B, C, R> func3(@NotNull final Function3<A, B, C, R> func) {
+    return (a, b, c) -> BlazeExecutor.getInstance().submit(() -> func.run(a, b, c));
+  }
+
+  private static <A, B, C, D, R> Func4<A, B, C, D, R> func4(@NotNull final Function4<A, B, C, D, R> func) {
+    return (a, b, c, d) -> BlazeExecutor.getInstance().submit(() -> func.run(a, b, c, d));
+  }
+
+  private static <A, B, C, D, E, R> Func5<A, B, C, D, E, R> func5(@NotNull final Function5<A, B, C, D, E, R> func) {
+    return (a, b, c, d, e) -> BlazeExecutor.getInstance().submit(() -> func.run(a, b, c, d, e));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ui/BlazeProblemsView.java b/blaze-base/src/com/google/idea/blaze/base/ui/BlazeProblemsView.java
new file mode 100644
index 0000000..0c65b61
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ui/BlazeProblemsView.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.ui;
+
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.util.UUID;
+
+public interface BlazeProblemsView {
+  @Nullable
+  static BlazeProblemsView getInstance(Project project) {
+    return ServiceManager.getService(project, BlazeProblemsView.class);
+  }
+
+  void clearOldMessages(UUID sessionId);
+  void addMessage(IssueOutput issue, UUID sessionId);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java b/blaze-base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java
new file mode 100644
index 0000000..bde6a7c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ui/BlazeValidationError.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ui;
+
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.concurrent.Immutable;
+import java.util.Collection;
+
+@Immutable
+public final class BlazeValidationError {
+
+  @NotNull
+  private final String error;
+
+  public BlazeValidationError(@NotNull String validationFailure) {
+    this.error = validationFailure;
+  }
+
+  @NotNull
+  public String getError() {
+    return error;
+  }
+
+  public static void collect(@Nullable Collection<BlazeValidationError> errors, @NotNull BlazeValidationError error) {
+    if (errors != null) {
+      errors.add(error);
+    }
+  }
+
+  public static void throwError(@NotNull Collection<BlazeValidationError> errors) throws IllegalArgumentException {
+    BlazeValidationError error = !errors.isEmpty() ? errors.iterator().next() : null;
+    String errorMessage = error != null ? error.getError() : "Unknown validation error";
+    throw new IllegalArgumentException(errorMessage);
+  }
+
+  /**
+   * Shows an error dialog.
+   *
+   * @return true if there are no errors
+   */
+  public static boolean verify(@NotNull Project project, @NotNull String title, @NotNull Collection<BlazeValidationError> errors) {
+    if (!errors.isEmpty()) {
+      BlazeValidationError error = errors.iterator().next();
+      Messages.showErrorDialog(project, error.getError(), title);
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java b/blaze-base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java
new file mode 100644
index 0000000..8965526
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ui/BlazeValidationResult.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ui;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Pair of (success, validation error)
+ */
+public class BlazeValidationResult {
+  public final boolean success;
+  @Nullable public final BlazeValidationError error;
+
+  private static final BlazeValidationResult SUCCESS = new BlazeValidationResult(true, null);
+
+  private BlazeValidationResult(boolean success, @Nullable BlazeValidationError error) {
+    this.success = success;
+    this.error = error;
+  }
+
+  public static BlazeValidationResult success() {
+    return SUCCESS;
+  }
+
+  public static BlazeValidationResult failure(BlazeValidationError error) {
+    return new BlazeValidationResult(false, error);
+  }
+
+  public static BlazeValidationResult failure(String error) {
+    return failure(new BlazeValidationError(error));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ui/ComboWrapper.java b/blaze-base/src/com/google/idea/blaze/base/ui/ComboWrapper.java
new file mode 100644
index 0000000..363660b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ui/ComboWrapper.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ui;
+
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.ui.ListCellRendererWrapper;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.event.ActionListener;
+import java.util.Collection;
+
+/**
+ * A simple wrapper for IDEA's {@link ComboBox} class adding type safety for the methods we commonly
+ * use.
+ */
+public final class ComboWrapper<T> {
+  @NotNull
+  private final ComboBox combo;
+
+  public static <T> ComboWrapper<T> create() {
+    return new ComboWrapper<T>();
+  }
+
+  private ComboWrapper() {
+    combo = new ComboBox();
+  }
+
+  public void setItems(@NotNull Collection<T> values) {
+    combo.setModel(new DefaultComboBoxModel(values.toArray()));
+  }
+
+  public void setSelectedItem(T value) {
+    combo.setSelectedItem(value);
+  }
+
+  public T getSelectedItem() {
+    return (T)combo.getSelectedItem();
+  }
+
+  public void addActionListener(@NotNull ActionListener listener) {
+    combo.addActionListener(listener);
+  }
+
+  public void setRenderer(ListCellRendererWrapper<T> renderer) {
+    combo.setRenderer(renderer);
+  }
+
+  @NotNull
+  public ComboBox getCombo() {
+    return combo;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ui/FileSelectorWithStoredHistory.java b/blaze-base/src/com/google/idea/blaze/base/ui/FileSelectorWithStoredHistory.java
new file mode 100644
index 0000000..5a1ac95
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ui/FileSelectorWithStoredHistory.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ui;
+
+import com.intellij.ide.util.BrowseFilesListener;
+import com.intellij.openapi.ui.ComponentWithBrowseButton;
+import com.intellij.openapi.ui.TextComponentAccessor;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.ui.TextFieldWithStoredHistory;
+
+import javax.annotation.Nullable;
+
+/**
+ * A file selector panel with text field, browse button and stored history.
+ */
+public class FileSelectorWithStoredHistory extends ComponentWithBrowseButton<TextFieldWithStoredHistory> {
+
+  public static FileSelectorWithStoredHistory create(String historyKey, String title) {
+    TextFieldWithStoredHistory textField = new TextFieldWithStoredHistory(historyKey);
+    return new FileSelectorWithStoredHistory(textField, title);
+  }
+
+  private FileSelectorWithStoredHistory(TextFieldWithStoredHistory textField, String title) {
+    super(textField, null);
+
+    addBrowseFolderListener(
+      title,
+      "",
+      null,
+      BrowseFilesListener.SINGLE_FILE_DESCRIPTOR,
+      TextComponentAccessor.TEXT_FIELD_WITH_STORED_HISTORY_WHOLE_TEXT);
+  }
+
+  /**
+   * Set the text without altering the history.
+   */
+  public void setText(@Nullable String text) {
+    if (text == null) {
+      getChildComponent().reset();
+    } else {
+      getChildComponent().setText(text);
+    }
+  }
+
+  public void setTextWithHistory(@Nullable String text) {
+    setText(text);
+    if (text != null) {
+      getChildComponent().addCurrentTextToHistory();
+    }
+  }
+
+  @Nullable
+  public String getText() {
+    String text = getChildComponent().getText();
+    return StringUtil.nullize(text);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/ui/IntegerTextField.java b/blaze-base/src/com/google/idea/blaze/base/ui/IntegerTextField.java
new file mode 100644
index 0000000..76f5245
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ui/IntegerTextField.java
@@ -0,0 +1,96 @@
+/*
+ * 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.ui;
+
+import javax.swing.*;
+import java.text.FieldPosition;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+
+/**
+ * Naive extension of JTextField, accepting integers or null.
+ */
+public class IntegerTextField extends JFormattedTextField {
+
+  private static final NumberFormat integerFormatter = new NullableNumberFormat(NumberFormat.getIntegerInstance());
+
+  private static class NullableNumberFormat extends NumberFormat {
+
+    private final NumberFormat base;
+
+    private NullableNumberFormat(NumberFormat base) {
+      this.base = base;
+    }
+
+    @Override
+    public Object parseObject(String source) throws ParseException {
+      if (source == null || source.trim().isEmpty()) {
+        return null;
+      }
+      return super.parseObject(source);
+    }
+
+    @Override
+    public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) {
+      return base.format(number, toAppendTo, pos);
+    }
+
+    @Override
+    public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) {
+      return base.format(number, toAppendTo, pos);
+    }
+
+    @Override
+    public Number parse(String source, ParsePosition parsePosition) {
+      return base.parse(source, parsePosition);
+    }
+  }
+
+  private int minValue = Integer.MIN_VALUE;
+  private int maxValue = Integer.MAX_VALUE;
+
+  public IntegerTextField() {
+    super(integerFormatter);
+  }
+
+  @Override
+  public void setValue(Object value) {
+    if (value == null) {
+      super.setValue(value);
+      return;
+    }
+    Integer integer;
+    try {
+      integer = Integer.parseInt(getFormatter().valueToString(value));
+
+    } catch (ParseException | NumberFormatException e) {
+      return; // retain existing value if invalid
+    }
+    super.setValue(integer < minValue ? minValue : integer > maxValue ? maxValue : integer);
+  }
+
+  public IntegerTextField setMinValue(int minValue) {
+    this.minValue = minValue;
+    return this;
+  }
+
+  public IntegerTextField setMaxValue(int maxValue) {
+    this.maxValue = maxValue;
+    return this;
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/ui/UiUtil.java b/blaze-base/src/com/google/idea/blaze/base/ui/UiUtil.java
new file mode 100644
index 0000000..05530bd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/ui/UiUtil.java
@@ -0,0 +1,83 @@
+/*
+ * 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.ui;
+
+import com.google.common.collect.Lists;
+import com.intellij.util.ui.GridBag;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * A collection of UI utility methods.
+ */
+public final class UiUtil {
+
+  public static final int INSETS = 7;
+
+  private UiUtil() {
+  }
+
+  public static Box createBox(@NotNull Component... components) {
+    return createBox(Lists.newArrayList(components));
+  }
+
+  /**
+   * Puts all the given components in order in a box, aligned left.
+   */
+  public static Box createBox(@NotNull Iterable<Component> components) {
+    Box box = Box.createVerticalBox();
+    box.setAlignmentX(0);
+    for (Component component : components) {
+      if (component instanceof JComponent) {
+        ((JComponent)component).setAlignmentX(0);
+      }
+      box.add(component);
+    }
+    return box;
+  }
+
+  @NotNull
+  public static GridBag getLabelConstraints(int indentLevel) {
+    Insets insets = new Insets(INSETS, INSETS + INSETS * indentLevel, 0, INSETS);
+    return new GridBag().anchor(GridBagConstraints.WEST).weightx(0).insets(insets);
+  }
+
+  @NotNull
+  public static GridBag getFillLineConstraints(int indentLevel) {
+    Insets insets = new Insets(INSETS, INSETS + INSETS * indentLevel, 0, INSETS);
+    return new GridBag().weightx(1).coverLine().fillCellHorizontally().anchor(GridBagConstraints.WEST).insets(insets);
+  }
+
+  public static void fillBottom(@NotNull JComponent component) {
+    component.add(Box.createVerticalGlue(), new GridBag().weightx(1).weighty(1).fillCell().coverLine());
+  }
+
+  public static void setEnabledRecursive(Component component, boolean enabled) {
+    component.setEnabled(enabled);
+    if (component instanceof Container) {
+      for (Component child : ((Container) component).getComponents()) {
+        setEnabledRecursive(child, enabled);
+      }
+    }
+  }
+
+  public static void setPreferredWidth(JComponent component, int width) {
+    int height = component.getPreferredSize().height;
+    component.setPreferredSize(new Dimension(width, height));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java b/blaze-base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java
new file mode 100644
index 0000000..f9028bd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/util/BlazeHelperBinaryUtil.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.util;
+
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.util.io.URLUtil;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.Map;
+
+public final class BlazeHelperBinaryUtil {
+
+  private static final Logger LOG = Logger.getInstance(BlazeHelperBinaryUtil.class);
+
+  private static final File tempDirectory = com.google.common.io.Files.createTempDir();
+  private static final Map<String, File> cachedFiles = new HashMap<>();
+
+  @Nullable
+  public static synchronized File getBlazeHelperBinary(@NotNull String binaryName) {
+    File file = cachedFiles.get(binaryName);
+    if (file != null) {
+      return file;
+    }
+    file = new File(tempDirectory, binaryName);
+    File directory = file.getParentFile();
+
+    if (!directory.mkdirs()) {
+      LOG.error("Could not create temporary dir: " + directory);
+      return null;
+    }
+
+    URL url = BlazeHelperBinaryUtil.class.getResource(binaryName);
+    if (url == null) {
+      LOG.error(String.format("Blaze binary '%s' was not found", binaryName));
+      return null;
+    }
+    try (InputStream inputStream = URLUtil.openResourceStream(url)) {
+      Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
+      file.setExecutable(true);
+      cachedFiles.put(binaryName, file);
+      return file;
+    } catch (IOException e) {
+      LOG.error(String.format("Error loading blaze binary '%s'", binaryName));
+      return null;
+    }
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/util/PackagePrefixCalculator.java b/blaze-base/src/com/google/idea/blaze/base/util/PackagePrefixCalculator.java
new file mode 100644
index 0000000..5ca01c7
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/util/PackagePrefixCalculator.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.util;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Calculates package prefix from workspace paths.
+ */
+public final class PackagePrefixCalculator {
+
+  public static String packagePrefixOf(@NotNull WorkspacePath workspacePath) {
+    int skipIndex = 0;
+
+    skipIndex = skipIndex == 0 ? skip(workspacePath, "java/") : skipIndex;
+    skipIndex = skipIndex == 0 ? skip(workspacePath, "javatests/") : skipIndex;
+
+    return workspacePath.relativePath().substring(skipIndex).replace('/', '.');
+  }
+
+  private static int skip(@NotNull WorkspacePath workspacePath, @NotNull String skipString) {
+    if (workspacePath.relativePath().startsWith(skipString)) {
+      return skipString.length();
+    }
+    return 0;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/util/SaveUtil.java b/blaze-base/src/com/google/idea/blaze/base/util/SaveUtil.java
new file mode 100644
index 0000000..b036308
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/util/SaveUtil.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.util;
+
+import com.intellij.openapi.fileEditor.FileDocumentManager;
+import com.intellij.util.ui.UIUtil;
+
+/**
+ * Utility for saving all files.
+ */
+public class SaveUtil {
+  public static void saveAllFiles() {
+    UIUtil.invokeAndWaitIfNeeded(new Runnable() {
+      @Override
+      public void run() {
+        FileDocumentManager.getInstance().saveAllDocuments();
+      }
+    });
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/util/SerializationUtil.java b/blaze-base/src/com/google/idea/blaze/base/util/SerializationUtil.java
new file mode 100644
index 0000000..4e64cfe
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/util/SerializationUtil.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.util;
+
+import com.google.common.io.Closeables;
+import com.intellij.CommonBundle;
+import com.intellij.openapi.diagnostic.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.*;
+
+/**
+ * Utils for serialization.
+ */
+public class SerializationUtil {
+  private static final Logger LOG = Logger.getInstance(SerializationUtil.class.getName());
+
+  public static void saveToDisk(@NotNull File file, @NotNull Serializable serializable) throws IOException {
+    ensureExists(file.getParentFile());
+    FileOutputStream fos = null;
+    try {
+      fos = new FileOutputStream(file);
+      ObjectOutputStream oos = new ObjectOutputStream(fos);
+      try {
+        oos.writeObject(serializable);
+      }
+      finally {
+        Closeables.close(oos, false);
+      }
+    }
+    finally {
+      Closeables.close(fos, false);
+    }
+  }
+
+  @Nullable
+  public static Object loadFromDisk(
+    @NotNull File file,
+    @NotNull final Iterable<ClassLoader> classLoaders) throws IOException {
+    try {
+      FileInputStream fin = null;
+      try {
+        if (!file.exists()) {
+          return null;
+        }
+        fin = new FileInputStream(file);
+        ObjectInputStream ois = new ObjectInputStream(fin) {
+          @Override
+          protected Class<?> resolveClass(ObjectStreamClass desc)
+            throws IOException, ClassNotFoundException {
+            String name = desc.getName();
+            for (ClassLoader loader : classLoaders) {
+              try {
+                return Class.forName(name, false, loader);
+              }
+              catch (ClassNotFoundException e) {
+                // Ignore - will throw eventually in super
+              }
+            }
+            return super.resolveClass(desc);
+          }
+        };
+        try {
+          return (Object) ois.readObject();
+        }
+        finally {
+          Closeables.close(ois, false);
+        }
+      }
+      finally {
+        Closeables.close(fin, false);
+      }
+    }
+    catch (ClassNotFoundException e) {
+      throw new IOException(e);
+    }
+    catch (ClassCastException e) {
+      throw new IOException(e);
+    }
+  }
+
+  private static void ensureExists(@NotNull File dir) throws IOException {
+    if (!dir.exists() && !dir.mkdirs()) {
+      throw new IOException(
+        CommonBundle.message("exception.directory.can.not.create", dir.getPath()));
+    }
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeDefaultVcsRootPolicy.java b/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeDefaultVcsRootPolicy.java
new file mode 100644
index 0000000..527321c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeDefaultVcsRootPolicy.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.vcs;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vcs.impl.DefaultVcsRootPolicy;
+import com.intellij.openapi.vcs.impl.projectlevelman.NewMappings;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Converts project-level mappings to actual VCS roots.
+ */
+public class BlazeDefaultVcsRootPolicy extends DefaultVcsRootPolicy {
+
+  @NotNull
+  private final Project project;
+
+  public BlazeDefaultVcsRootPolicy(@NotNull Project project) {
+    this.project = project;
+  }
+
+  @Override
+  public void addDefaultVcsRoots(
+    NewMappings mappingList,
+    @NotNull String vcsName,
+    List<VirtualFile> result) {
+    result.addAll(getVcsRoots());
+  }
+
+  @NotNull
+  @Override
+  public Collection<VirtualFile> getDirtyRoots() {
+    return getVcsRoots();
+  }
+
+  private List<VirtualFile> getVcsRoots() {
+    List<VirtualFile> result = Lists.newArrayList();
+    BlazeImportSettings importSettings = BlazeImportSettingsManager.getInstance(project)
+      .getImportSettings();
+    if (importSettings == null) {
+      return result;
+    }
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    if (projectViewSet == null) {
+      return result;
+    }
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+
+    for (DirectoryEntry entry : projectViewSet.listItems(DirectorySection.KEY)) {
+      if (!entry.included) {
+        continue;
+      }
+      File packageDir = workspaceRoot.fileForPath(entry.directory);
+
+      VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(packageDir);
+      if (virtualFile != null) {
+        result.add(virtualFile);
+      }
+    }
+    return result;
+  }
+
+  @Override
+  public boolean matchesDefaultMapping(final VirtualFile file, final Object matchContext) {
+    for (VirtualFile directory : getVcsRoots()) {
+      if (VfsUtilCore.isAncestor(directory, file, false)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  @Nullable
+  public Object getMatchContext(final VirtualFile file) {
+    return null;
+  }
+
+  @Override
+  @Nullable
+  public VirtualFile getVcsRootFor(final VirtualFile file) {
+    for (VirtualFile directory : getVcsRoots()) {
+      if (VfsUtilCore.isAncestor(directory, file, false)) {
+        return directory;
+      }
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeVcsHandler.java b/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeVcsHandler.java
new file mode 100644
index 0000000..973ce16
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeVcsHandler.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.vcs;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides a diff against the version control system.
+ */
+public interface BlazeVcsHandler {
+  ExtensionPointName<BlazeVcsHandler> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.VcsHandler");
+
+  /**
+   * Optionally returns a client name if the supplied workspace root corresponds to this VCS type.
+   */
+  @Nullable
+  String getClientName(WorkspaceRoot workspaceRoot);
+
+  /**
+   * Returns whether this vcs handler can manage this project
+   */
+  boolean handlesProject(Project project, WorkspaceRoot workspaceRoot);
+
+  /**
+   * Returns the working set of modified files compared to some "upstream".
+   */
+  ListenableFuture<WorkingSet> getWorkingSet(Project project,
+                                             WorkspaceRoot workspaceRoot,
+                                             ListeningExecutorService executor);
+
+  /**
+   * Optionally creates a sync handler to perform vcs-specific computation during sync.
+   */
+  @Nullable
+  BlazeVcsSyncHandler createSyncHandler(Project project, WorkspaceRoot workspaceRoot);
+
+  interface BlazeVcsSyncHandler {
+    enum ValidationResult {
+      OK,
+      Error,
+      RestartSync, /** The sync process needs restarting **/
+    }
+
+    /**
+     * Updates the vcs state of the project.
+     *
+     * @return True for OK, false to abort the sync process.
+     */
+    boolean update(BlazeContext context, BlazeRoots blazeRoots, ListeningExecutorService executor);
+
+    /**
+     * Returns a custom workspace path resolver for this vcs.
+     */
+    @Nullable
+    WorkspacePathResolver getWorkspacePathResolver();
+
+    /**
+     * Validates the project view. Can cause sync to fail or restart.
+     */
+    ValidationResult validateProjectView(BlazeContext context, ProjectViewSet projectViewSet);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeVcsHelper.java b/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeVcsHelper.java
new file mode 100644
index 0000000..809f88a
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/vcs/BlazeVcsHelper.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.vcs;
+
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility methods for VCS-specific functionality.
+ */
+public class BlazeVcsHelper {
+
+  @Nullable
+  public static String getClientName(WorkspaceRoot workspaceRoot) {
+    for (BlazeVcsHandler candidate : BlazeVcsHandler.EP_NAME.getExtensions()) {
+      String name = candidate.getClientName(workspaceRoot);
+      if (name != null) {
+        return name;
+      }
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/vcs/FallbackBlazeVcsHandler.java b/blaze-base/src/com/google/idea/blaze/base/vcs/FallbackBlazeVcsHandler.java
new file mode 100644
index 0000000..5a9ea64
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/vcs/FallbackBlazeVcsHandler.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.vcs;
+
+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.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+/**
+ * Used for bazel projects, when no other vcs handler can be found. Fallback to returning a null working set.
+ */
+public class FallbackBlazeVcsHandler implements BlazeVcsHandler {
+  @Nullable
+  @Override
+  public String getClientName(WorkspaceRoot workspaceRoot) {
+    return null;
+  }
+
+  @Override
+  public boolean handlesProject(Project project, WorkspaceRoot workspaceRoot) {
+    return Blaze.getBuildSystem(project) == BuildSystem.Bazel;
+  }
+
+  @Override
+  public ListenableFuture<WorkingSet> getWorkingSet(Project project, WorkspaceRoot workspaceRoot, ListeningExecutorService executor) {
+    return Futures.immediateFuture(null);
+  }
+
+  @Nullable
+  @Override
+  public BlazeVcsSyncHandler createSyncHandler(Project project, WorkspaceRoot workspaceRoot) {
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/vcs/VcsWorkspacePathResolver.java b/blaze-base/src/com/google/idea/blaze/base/vcs/VcsWorkspacePathResolver.java
new file mode 100644
index 0000000..b6cf25b
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/vcs/VcsWorkspacePathResolver.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.vcs;
+
+import javax.annotation.Nullable;
+import java.io.File;
+
+/**
+ * Created by tomlu on 5/13/16.
+ */
+public interface VcsWorkspacePathResolver {
+
+  @Nullable
+  File findPackageRoot(String relativePath);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitBlazeVcsHandler.java b/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitBlazeVcsHandler.java
new file mode 100644
index 0000000..322e91f
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitBlazeVcsHandler.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.vcs.git;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.vcs.BlazeVcsHandler;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+
+import javax.annotation.Nullable;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+
+/**
+ * Vcs diff provider for git
+ */
+public class GitBlazeVcsHandler implements BlazeVcsHandler {
+
+  private static final Logger LOG = Logger.getInstance(GitBlazeVcsHandler.class);
+
+  @Nullable
+  @Override
+  public String getClientName(WorkspaceRoot workspaceRoot) {
+    return null;
+  }
+
+  @Override
+  public boolean handlesProject(Project project, WorkspaceRoot workspaceRoot) {
+    return Blaze.getBuildSystem(project) == BuildSystem.Bazel
+      && isGitRepository(workspaceRoot)
+      && tracksRemote(workspaceRoot);
+  }
+
+  @Override
+  public ListenableFuture<WorkingSet> getWorkingSet(Project project,
+                                                    WorkspaceRoot workspaceRoot,
+                                                    ListeningExecutorService executor) {
+    return executor.submit(() -> {
+      String upstreamSha = getUpstreamSha(workspaceRoot, false);
+      if (upstreamSha == null) {
+        return null;
+      }
+      return GitDiffProvider.calculateDiff(workspaceRoot, upstreamSha);
+    });
+  }
+
+  @Nullable
+  @Override
+  public BlazeVcsSyncHandler createSyncHandler(Project project,
+                                               WorkspaceRoot workspaceRoot) {
+    return null;
+  }
+
+  private static boolean isGitRepository(WorkspaceRoot workspaceRoot) {
+    // TODO: What if the git repo root is a parent directory of the workspace root?
+    // Just call 'git rev-parse --is-inside-work-tree' or similar instead?
+    File gitDir = new File(workspaceRoot.directory(), ".git");
+    return FileAttributeProvider.getInstance().isDirectory(gitDir);
+  }
+
+  /**
+   * If we're not on a git branch which tracks a remote, we have no way of determining a WorkingSet.
+   */
+  private static boolean tracksRemote(WorkspaceRoot workspaceRoot) {
+    return getUpstreamSha(workspaceRoot, true) != null;
+  }
+
+  /**
+   * Returns the git commit SHA corresponding to the most recent commit
+   * in the current branch which matches a commit in the currently-tracked remote branch.
+   */
+  @Nullable
+  public static String getUpstreamSha(WorkspaceRoot workspaceRoot, boolean suppressErrors) {
+    return getConsoleOutput(workspaceRoot, ImmutableList.of("git", "rev-parse", "@{u}"), suppressErrors);
+  }
+
+  /**
+   * @return the console output, in string form, or null if there was a non-zero exit code.
+   */
+  @Nullable
+  private static String getConsoleOutput(WorkspaceRoot workspaceRoot, ImmutableList<String> command, boolean suppressErrors) {
+    ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+    ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+    int retVal = ExternalTask.builder(workspaceRoot, command)
+      .stdout(stdout)
+      .stderr(stderr)
+      .build()
+      .run();
+    if (retVal != 0) {
+      if (!suppressErrors) {
+        LOG.error(stderr);
+      }
+      return null;
+    }
+    return StringUtil.trimEnd(stdout.toString(), "\n");
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitDiffProvider.java b/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitDiffProvider.java
new file mode 100644
index 0000000..1981ae5
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitDiffProvider.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.vcs.git;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.text.StringUtil;
+
+import javax.annotation.Nullable;
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Vcs diff provider for git.
+ */
+public class GitDiffProvider {
+
+  private static final Logger LOG = Logger.getInstance(GitDiffProvider.class);
+
+  /**
+   * Finds all changes between HEAD and the git commit specified by
+   * the provided SHA.<br>
+   * Returns null if an error occurred.
+   */
+  @Nullable
+  public static WorkingSet calculateDiff(
+    WorkspaceRoot workspaceRoot,
+    String upstreamSha) {
+
+    String gitRoot = getConsoleOutput(workspaceRoot, "git", "rev-parse", "--show-toplevel");
+    if (gitRoot == null) {
+      return null;
+    }
+    GitStatusLineProcessor processor = new GitStatusLineProcessor(workspaceRoot, gitRoot);
+    ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+    // Do a git diff to find all modified files we know about
+    int retVal = ExternalTask.builder(workspaceRoot, ImmutableList.of("git", "diff", "--name-status", "--no-renames", upstreamSha))
+      .stdout(LineProcessingOutputStream.of(processor))
+      .stderr(stderr)
+      .build()
+      .run();
+    if (retVal != 0) {
+      LOG.error(stderr);
+      return null;
+    }
+
+    // Finally list all untracked files, as they're not caught by the git diff step above
+    String untrackedFilesOutput = getConsoleOutput(workspaceRoot, "git", "ls-files", "--others", "--exclude-standard");
+    if (untrackedFilesOutput == null) {
+      return null;
+    }
+
+    List<WorkspacePath> untrackedFiles = Arrays.asList(untrackedFilesOutput.split("\n"))
+      .stream()
+      .filter(s -> !Strings.isNullOrEmpty(s))
+      .filter(WorkspacePath::validate)
+      .map(WorkspacePath::new)
+      .collect(Collectors.toList());
+
+    return new WorkingSet(
+      ImmutableList.<WorkspacePath>builder().addAll(processor.addedFiles).addAll(untrackedFiles).build(),
+      ImmutableList.copyOf(processor.modifiedFiles),
+      ImmutableList.copyOf(processor.deletedFiles)
+    );
+  }
+
+  /**
+   * @return the console output, in string form, or null if there was a non-zero exit code.
+   */
+  @Nullable
+  private static String getConsoleOutput(WorkspaceRoot workspaceRoot, String... commands) {
+    ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+    ByteArrayOutputStream stderr = new ByteArrayOutputStream();
+
+    int retVal = ExternalTask.builder(workspaceRoot, ImmutableList.copyOf(commands))
+      .stdout(stdout)
+      .stderr(stderr)
+      .build()
+      .run();
+    if (retVal != 0) {
+      LOG.error(stderr);
+      return null;
+    }
+    return StringUtil.trimEnd(stdout.toString(), "\n");
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessor.java b/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessor.java
new file mode 100644
index 0000000..aaf225e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessor.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.vcs.git;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.util.text.StringUtil;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class GitStatusLineProcessor implements LineProcessingOutputStream.LineProcessor {
+
+  private static final Pattern REGEX = Pattern.compile("^(A|M|D)\\s*(.*?)$");
+
+  private final WorkspaceRoot workspaceRoot;
+  private final String gitRoot;
+
+  public final List<WorkspacePath> addedFiles = Lists.newArrayList();
+  public final List<WorkspacePath> modifiedFiles = Lists.newArrayList();
+  public final List<WorkspacePath> deletedFiles = Lists.newArrayList();
+
+  public GitStatusLineProcessor(WorkspaceRoot workspaceRoot, String gitRoot) {
+    this.workspaceRoot = workspaceRoot;
+    this.gitRoot = gitRoot;
+  }
+
+  @Override
+  public boolean processLine(String line) {
+    Matcher matcher = REGEX.matcher(line);
+    if (matcher.find()) {
+      String type = matcher.group(1);
+      String file = matcher.group(2);
+      file = StringUtil.trimEnd(file, '/');
+
+      WorkspacePath workspacePath = getWorkspacePath(file);
+      if (workspacePath == null) {
+        return true;
+      }
+      switch (type) {
+        case "A":
+          addedFiles.add(workspacePath);
+          break;
+        case "M":
+          modifiedFiles.add(workspacePath);
+          break;
+        case "D":
+          deletedFiles.add(workspacePath);
+          break;
+      }
+    }
+    return true;
+  }
+
+  @Nullable
+  private WorkspacePath getWorkspacePath(String gitPath) {
+    File absoluteFile = new File(gitRoot, gitPath);
+    if (workspaceRoot.isInWorkspace(absoluteFile)) {
+      return workspaceRoot.workspacePathFor(absoluteFile);
+    }
+    return null;
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeImportFileChooser.java b/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeImportFileChooser.java
new file mode 100644
index 0000000..f9ef3f3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeImportFileChooser.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.wizard;
+
+import com.intellij.ide.util.PropertiesComponent;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDialog;
+import com.intellij.openapi.fileChooser.FileChooserFactory;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NonNls;
+
+import javax.annotation.Nullable;
+
+public final class BlazeImportFileChooser {
+  private static final String WIZARD_TITLE = "Import Blaze Project";
+  private static final String WIZARD_DESCRIPTION = "Select a workspace, a .blazeproject file, or a BUILD file to import";
+  @NonNls
+  private static final String LAST_IMPORTED_LOCATION = "last.imported.location";
+
+
+  private static final class BlazeFileChooser extends FileChooserDescriptor {
+    BlazeFileChooser() {
+      super(true, 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) && ImportSource.canImport(file);
+    }
+  }
+
+  private static FileChooserDescriptor createFileChooserDescriptor() {
+    return new BlazeFileChooser()
+      .withShowHiddenFiles(true) // Show root project view file
+      .withHideIgnored(false)
+      .withTitle(WIZARD_TITLE)
+      .withDescription(WIZARD_DESCRIPTION)
+      .withFileFilter(ImportSource::canImport);
+  }
+
+  @Nullable
+  public static VirtualFile getFileToImport() {
+    FileChooserDescriptor descriptor = createFileChooserDescriptor();
+    FileChooserDialog chooser = FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
+    VirtualFile toSelect = null;
+    String lastLocation = PropertiesComponent.getInstance().getValue(LAST_IMPORTED_LOCATION);
+    if (lastLocation != null) {
+      toSelect = LocalFileSystem.getInstance().refreshAndFindFileByPath(lastLocation);
+    }
+    VirtualFile[] files = chooser.choose(null, toSelect);
+    if (files.length == 0) {
+      return null;
+    }
+    VirtualFile file = files[0];
+    PropertiesComponent.getInstance().setValue(LAST_IMPORTED_LOCATION, file.getPath());
+    return file;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeNewProjectBuilder.java b/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeNewProjectBuilder.java
new file mode 100644
index 0000000..3559ffd
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeNewProjectBuilder.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.wizard;
+
+import com.google.idea.blaze.base.plugin.dependency.PluginDependencyHelper;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+public final class BlazeNewProjectBuilder {
+  private static final Logger LOG = Logger.getInstance(BlazeNewProjectBuilder.class);
+
+  public static List<Module> commit(final Project project, BlazeImportSettings importSettings, ProjectView projectView) {
+    String projectDataDirectory = importSettings.getProjectDataDirectory();
+
+    if (!StringUtil.isEmpty(projectDataDirectory)) {
+      File projectDataDir = new File(projectDataDirectory);
+      if (!projectDataDir.exists()) {
+        if (!projectDataDir.mkdirs()) {
+          LOG.error("Unable to create the project directory: " + projectDataDirectory);
+        }
+      }
+    }
+
+    BlazeImportSettingsManager.getInstance(project).setImportSettings(importSettings);
+
+    try {
+      String projectViewFile = importSettings.getProjectViewFile();
+      LOG.assertTrue(projectViewFile != null);
+      ProjectViewStorageManager.getInstance().writeProjectView(
+          ProjectViewParser.projectViewToString(projectView),
+          new File(projectViewFile)
+      );
+    } catch (IOException e) {
+      LOG.error(e);
+    }
+
+    PluginDependencyHelper.addDependencyOnSyncPlugin(project);
+
+    // Initial sync of the project happens in BlazeSyncStartupActivity
+
+    return Collections.emptyList();
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeProjectSettingsControl.java b/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeProjectSettingsControl.java
new file mode 100644
index 0000000..9ebc727
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard/BlazeProjectSettingsControl.java
@@ -0,0 +1,402 @@
+/*
+ * 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.wizard;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+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.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+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.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.projectview.section.sections.ImportSection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.ui.JPanelProvidingProject;
+import com.google.idea.blaze.base.settings.ui.ProjectViewUi;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.google.idea.blaze.base.vcs.BlazeVcsHelper;
+import com.intellij.ide.RecentProjectsManager;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationNamesInfo;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.ui.TextComponentAccessor;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.util.SystemProperties;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * The UI control to collect project settings when importing a Blaze project.
+ */
+public final class BlazeProjectSettingsControl {
+
+  private static final FileChooserDescriptor STUDIO_PROJECT_FOLDER_DESCRIPTOR =
+    new FileChooserDescriptor(false, true, false, false, false, false);
+  private static final Logger LOG = Logger.getInstance(BlazeProjectSettingsControl.class);
+
+  private WorkspaceRoot workspaceRoot;
+  @Nullable private File sharedProjectViewFile;
+  @Nullable private String vcsClientName;
+
+  private TextFieldWithBrowseButton projectDataDirField;
+  private JTextField projectNameField;
+  private ProjectViewUi projectViewUi;
+
+  public BlazeProjectSettingsControl(Disposable parentDisposable) {
+    this.projectViewUi = new ProjectViewUi(parentDisposable);
+  }
+
+  public JPanel createComponent(File fileToImport) {
+    JPanel component = new JPanelProvidingProject(ProjectViewUi.getProject(), new GridBagLayout());
+    fillUi(component, 0);
+    init(fileToImport);
+    UiUtil.fillBottom(component);
+    return component;
+  }
+
+  private void fillUi(@NotNull JPanel canvas, int indentLevel) {
+    JLabel projectDataDirLabel = new JBLabel("Project data directory:");
+
+    Dimension minSize = ProjectViewUi.getMinimumSize();
+    // Add 120 pixels so we have room for our extra fields
+    minSize.setSize(minSize.width, minSize.height + 120);
+    canvas.setMinimumSize(minSize);
+    canvas.setPreferredSize(minSize);
+
+    projectDataDirField = new TextFieldWithBrowseButton();
+    projectDataDirField
+      .addBrowseFolderListener("", "Blaze Android Studio project data directory", null,
+                               STUDIO_PROJECT_FOLDER_DESCRIPTOR,
+                               TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, false);
+    final String dataDirToolTipText =
+      "Directory in which to store the project's metadata. Choose a directory outside of"
+      + " the Piper/CitC client directories or Git5 directory.";
+    projectDataDirField.setToolTipText(dataDirToolTipText);
+    projectDataDirLabel.setToolTipText(dataDirToolTipText);
+
+    canvas.add(projectDataDirLabel, UiUtil.getLabelConstraints(indentLevel));
+    canvas.add(projectDataDirField, UiUtil.getFillLineConstraints(0));
+
+    JLabel projectNameLabel = new JLabel("Project name:");
+    projectNameField = new JTextField();
+    final String projectNameToolTipText =
+      "Project display name.";
+    projectNameField.setToolTipText(projectNameToolTipText);
+    projectNameLabel.setToolTipText(projectNameToolTipText);
+    canvas.add(projectNameLabel, UiUtil.getLabelConstraints(indentLevel));
+    canvas.add(projectNameField, UiUtil.getFillLineConstraints(0));
+
+    projectViewUi.fillUi(canvas, indentLevel);
+  }
+
+  private void init(File fileToImport) {
+    workspaceRoot = BuildSystemProvider.getWorkspaceRootProvider(BuildSystem.Blaze).findWorkspaceRoot(fileToImport);
+    if (workspaceRoot == null) {
+      throw new IllegalArgumentException("Invalid workspace root: " + fileToImport);
+    }
+    vcsClientName = BlazeVcsHelper.getClientName(workspaceRoot);
+
+    File importDirectory = null;
+    if (ProjectViewStorageManager.isProjectViewFile(fileToImport.getPath())) {
+      importDirectory = fileToImport.getParentFile();
+      sharedProjectViewFile = new File(fileToImport.getPath());
+    }
+    else if (ImportSource.isBuildFile(fileToImport)) {
+      importDirectory = fileToImport.getParentFile();
+      for (String extension : ProjectViewStorageManager.VALID_EXTENSIONS) {
+        File defaultProjectViewFile = new File(fileToImport.getParentFile(), "." + extension);
+        if (defaultProjectViewFile.exists()) {
+          sharedProjectViewFile = defaultProjectViewFile;
+          break;
+        }
+      }
+    }
+
+    String defaultProjectName = importDirectory != null
+                                ? importDirectory.getName()
+                                : workspaceRoot.directory().getParentFile().getName();
+    projectNameField.setText(defaultProjectName);
+
+    String defaultDataDir = getDefaultProjectDataDirectory(defaultProjectName, vcsClientName);
+    projectDataDirField.setText(defaultDataDir);
+
+    String projectViewText = "";
+    if (sharedProjectViewFile != null) {
+      try {
+        projectViewText = ProjectViewStorageManager.getInstance().loadProjectView(sharedProjectViewFile);
+        if (projectViewText == null) {
+          LOG.error("Could not load project view: " + sharedProjectViewFile);
+          projectViewText = "";
+        }
+      }
+      catch (IOException e) {
+        LOG.error(e);
+      }
+    }
+    if (projectViewText.isEmpty() && importDirectory != null) {
+      projectViewText = guessProjectViewFromLocation(workspaceRoot, importDirectory);
+    }
+
+    projectViewUi.init(
+      workspaceRoot,
+      projectViewText,
+      sharedProjectViewFile != null ? projectViewText : null,
+      sharedProjectViewFile,
+      sharedProjectViewFile != null,
+      false /* allowEditShared - not allowed during import */
+    );
+  }
+
+
+  @NotNull
+  private static String getDefaultProjectDataDirectory(@NotNull String projectName, @Nullable String vcsClientName) {
+    File defaultDataDirectory = new File(getDefaultProjectsDirectory());
+    if (vcsClientName != null) {
+      // Ensure that each client gets its own data directory.
+      projectName = vcsClientName + "-" + projectName;
+    }
+    File desiredLocation = new File(defaultDataDirectory, projectName);
+    return newUniquePath(desiredLocation);
+  }
+
+  @NotNull
+  private static String getDefaultProjectsDirectory() {
+    final String lastProjectLocation = RecentProjectsManager.getInstance().getLastProjectCreationLocation();
+    if (lastProjectLocation != null) {
+      return lastProjectLocation.replace('/', File.separatorChar);
+    }
+    final String userHome = SystemProperties.getUserHome();
+    String productName = ApplicationNamesInfo.getInstance().getLowercaseProductName();
+    return userHome.replace('/', File.separatorChar) + File.separator + productName.replace(" ", "") + "Projects";
+  }
+
+  @NotNull
+  private static String guessProjectViewFromLocation(
+    @NotNull WorkspaceRoot workspaceRoot,
+    @NotNull File importDirectory) {
+
+    WorkspacePath mainModuleGoogle3RelativePath = workspaceRoot.workspacePathFor(importDirectory);
+    WorkspacePath testModuleGoogle3RelativePath = guessTestRelativePath(
+      workspaceRoot,
+      mainModuleGoogle3RelativePath);
+
+    ListSection.Builder<DirectoryEntry> directorySectionBuilder = ListSection.builder(DirectorySection.KEY);
+    directorySectionBuilder.add(DirectoryEntry.include(mainModuleGoogle3RelativePath));
+    if (testModuleGoogle3RelativePath != null) {
+      directorySectionBuilder.add(DirectoryEntry.include(testModuleGoogle3RelativePath));
+    }
+
+    ListSection.Builder<TargetExpression> targetSectionBuilder = ListSection.builder(TargetSection.KEY);
+    targetSectionBuilder.add(TargetExpression.fromString("//" + mainModuleGoogle3RelativePath + "/...:all"));
+    if (testModuleGoogle3RelativePath != null) {
+      targetSectionBuilder.add(TargetExpression.fromString("//" + testModuleGoogle3RelativePath + "/...:all"));
+    }
+
+    return ProjectViewParser.projectViewToString(
+      ProjectView.builder()
+        .put(directorySectionBuilder)
+        .put(targetSectionBuilder)
+        .build()
+    );
+  }
+
+  @Nullable
+  private static WorkspacePath guessTestRelativePath(
+    @NotNull WorkspaceRoot workspaceRoot,
+    @NotNull WorkspacePath projectWorkspacePath) {
+    String projectRelativePath = projectWorkspacePath.relativePath();
+    String testBuildFileRelativePath = null;
+    if (projectRelativePath.startsWith("java/")) {
+      testBuildFileRelativePath = projectRelativePath.replaceFirst("java/", "javatests/");
+    }
+    else if (projectRelativePath.contains("/java/")) {
+      testBuildFileRelativePath = projectRelativePath.replaceFirst("/java/", "/javatests/");
+    }
+    if (testBuildFileRelativePath != null) {
+      File testBuildFile = workspaceRoot.fileForPath(new WorkspacePath(testBuildFileRelativePath));
+      if (testBuildFile.exists()) {
+        return new WorkspacePath(testBuildFileRelativePath);
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns a unique file path by appending numbers until a non-collision is found.
+   */
+  private static String newUniquePath(File location) {
+    if (!location.exists()) {
+      return location.getAbsolutePath();
+    }
+
+    String name = location.getName();
+    File directory = location.getParentFile();
+    int tries = 0;
+    while (true) {
+      String candidateName = String.format("%s-%02d", name, tries);
+      File candidateFile = new File(directory, candidateName);
+      if (!candidateFile.exists()) {
+        return candidateFile.getAbsolutePath();
+      }
+      tries++;
+    }
+  }
+
+  @Nullable
+  private BlazeValidationError validateProjectDataDirectory(@NotNull File projectDataDirPath) {
+    if (workspaceRoot.isInWorkspace(projectDataDirPath)) {
+      return new BlazeValidationError(
+        "Project data directory should be placed outside of the client directory"
+      );
+    }
+    return null;
+  }
+
+  @NotNull
+  public BlazeValidationResult validate() {
+    // Validate project settings fields
+    String projectName = projectNameField.getText().trim();
+    if (StringUtil.isEmpty(projectName)) {
+      return BlazeValidationResult.failure(new BlazeValidationError("Project name is not specified"));
+    }
+    String projectDataDirPath = projectDataDirField.getText().trim();
+    if (StringUtil.isEmpty(projectDataDirPath)) {
+      return BlazeValidationResult.failure(new BlazeValidationError("Project data directory is not specified"));
+    }
+    File projectDataDir = new File(projectDataDirPath);
+    if (!projectDataDir.isAbsolute()) {
+      return BlazeValidationResult.failure(new BlazeValidationError("Project data directory is not valid"));
+    }
+    BlazeValidationError projectDataDirectoryValidation = validateProjectDataDirectory(projectDataDir);
+    if (projectDataDirectoryValidation != null) {
+      return BlazeValidationResult.failure(projectDataDirectoryValidation);
+    }
+
+    List<IssueOutput> issues = Lists.newArrayList();
+
+    ProjectViewSet projectViewSet = projectViewUi.parseProjectView(issues);
+    BlazeValidationError projectViewParseError = validationErrorFromIssueList(issues);
+    if (projectViewParseError != null) {
+      return BlazeValidationResult.failure(projectViewParseError);
+    }
+
+    return BlazeValidationResult.success();
+  }
+
+  public ImportResults getResults() {
+    String projectName = projectNameField.getText().trim();
+    String projectDataDirectory = projectDataDirField.getText().trim();
+
+    // Create unique location hash
+    final String locationHash = createLocationHash(projectName);
+
+    // Only support blaze in the old import wizard. TODO: remove this wizard prior to public bazel release.
+    BuildSystem fixedBuildSystem = BuildSystem.Blaze;
+
+    File sharedProjectViewFile = this.sharedProjectViewFile;
+    File localProjectViewFile = ProjectViewStorageManager.getLocalProjectViewFileName(fixedBuildSystem, new File(projectDataDirectory));
+
+    BlazeImportSettings importSettings = new BlazeImportSettings(
+      workspaceRoot.directory().getPath(),
+      projectName,
+      projectDataDirectory,
+      locationHash,
+      localProjectViewFile.getPath(),
+      fixedBuildSystem
+    );
+
+    boolean useSharedProjectView = projectViewUi.getUseSharedProjectView();
+
+    // If we're using a shared project view, synthesize a local one that imports the shared one
+    final ProjectView projectView;
+    if (useSharedProjectView && sharedProjectViewFile != null) {
+      WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+      projectView = ProjectView.builder()
+        .put(ScalarSection.builder(ImportSection.KEY)
+          .set(workspaceRoot.workspacePathFor(sharedProjectViewFile)))
+        .build();
+    } else {
+      ProjectViewSet parseResult = projectViewUi.parseProjectView(Lists.<IssueOutput>newArrayList());
+      ProjectViewSet.ProjectViewFile projectViewFile = parseResult.getTopLevelProjectViewFile();
+      assert projectViewFile != null;
+      projectView = projectViewFile.projectView;
+    }
+
+    return new ImportResults(
+      importSettings,
+      projectView,
+      calculateProjectName(projectName, vcsClientName),
+      projectDataDirectory
+    );
+  }
+
+  @NotNull
+  private static String createLocationHash(@NotNull String projectName) {
+    String uuid = UUID.randomUUID().toString();
+    uuid = uuid.substring(0, Math.min(uuid.length(), 8));
+    return projectName.replaceAll("[^a-zA-Z0-9]", "") + "-" + uuid;
+  }
+
+  private static String calculateProjectName(
+    @NotNull String projectName,
+    @Nullable String vcsClientName) {
+    if (vcsClientName != null) {
+      projectName = String.format("%s (%s)", projectName, vcsClientName);
+    }
+    return projectName;
+  }
+
+  @Nullable
+  private static BlazeValidationError validationErrorFromIssueList(List<IssueOutput> issues) {
+    List<IssueOutput> errors = issues
+      .stream()
+      .filter(issue -> issue.getCategory() == IssueOutput.Category.ERROR)
+      .collect(Collectors.toList());
+
+    if (!errors.isEmpty()) {
+      StringBuilder errorMessage = new StringBuilder();
+      errorMessage.append("The following issues were found:\n\n");
+      for (IssueOutput issue : errors) {
+        errorMessage.append(issue.getMessage());
+        errorMessage.append('\n');
+      }
+      return new BlazeValidationError(errorMessage.toString());
+    }
+    return null;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard/ImportResults.java b/blaze-base/src/com/google/idea/blaze/base/wizard/ImportResults.java
new file mode 100644
index 0000000..2d99054
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard/ImportResults.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.wizard;
+
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public final class ImportResults {
+
+  public final BlazeImportSettings importSettings;
+  public final ProjectView projectView;
+  public final String projectName;
+  public final String projectDataDirectory;
+
+  public ImportResults(
+    BlazeImportSettings importSettings,
+    ProjectView projectView,
+    String projectName,
+    String projectDataDirectory
+  ) {
+    this.importSettings = importSettings;
+    this.projectView = projectView;
+    this.projectName = projectName;
+    this.projectDataDirectory = projectDataDirectory;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard/ImportSource.java b/blaze-base/src/com/google/idea/blaze/base/wizard/ImportSource.java
new file mode 100644
index 0000000..986ddef
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard/ImportSource.java
@@ -0,0 +1,51 @@
+/*
+ * 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.wizard;
+
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.bazel.WorkspaceRootProvider;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+
+/**
+ * Some convenience methods regarding your import source.
+ */
+public class ImportSource {
+  static boolean isBuildFile(@NotNull File file) {
+    return file.getName().equals("BUILD");
+  }
+
+  static boolean canImport(@NotNull File file) {
+    WorkspaceRootProvider helper = BuildSystemProvider.getWorkspaceRootProvider(BuildSystem.Blaze);
+    if (helper.isWorkspaceRoot(file)) {
+      return true;
+    }
+    if (ProjectViewStorageManager.isProjectViewFile(file.getName()) || isBuildFile(file)) {
+      if (helper.isInWorkspace(file)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public static boolean canImport(@NotNull VirtualFile file) {
+    return canImport(new File(file.getPath()));
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BazelWizardOptionProvider.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BazelWizardOptionProvider.java
new file mode 100644
index 0000000..e0c4ba4
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BazelWizardOptionProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.wizard2;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Collection;
+
+/**
+ * Provides bazel options for the wizard.
+ */
+public class BazelWizardOptionProvider implements BlazeWizardOptionProvider {
+
+  @Override
+  public Collection<BlazeSelectWorkspaceOption> getSelectWorkspaceOptions(BlazeNewProjectBuilder builder) {
+    return ImmutableList.of(
+      new UseExistingBazelWorkspaceOption(builder)
+    );
+  }
+
+  @Override
+  public Collection<BlazeSelectProjectViewOption> getSelectProjectViewOptions(BlazeNewProjectBuilder builder) {
+    return ImmutableList.of(
+      new CreateFromScratchProjectViewOption(),
+      new ImportFromWorkspaceProjectViewOption(builder),
+      new GenerateFromBuildFileSelectProjectViewOption(builder),
+      new CopyExternalProjectViewOption(builder)
+    );
+  }
+
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java
new file mode 100644
index 0000000..2738b20
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeNewProjectBuilder.java
@@ -0,0 +1,200 @@
+/*
+ * 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.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.plugin.dependency.PluginDependencyHelper;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.text.StringUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * Contains the state to build a new project throughout the new project wizard process.
+ */
+public final class BlazeNewProjectBuilder {
+  private static final Logger LOG = Logger.getInstance(BlazeNewProjectBuilder.class);
+
+  // Stored in user settings as the last imported workspace
+  private static final String LAST_IMPORTED_BLAZE_WORKSPACE = "blaze-wizard.last-imported-workspace";
+  private static final String LAST_IMPORTED_BAZEL_WORKSPACE = "blaze-wizard.last-imported-bazel-workspace";
+
+  public static String lastImportedWorkspaceKey(BuildSystem buildSystem) {
+    switch (buildSystem) {
+      case Blaze: return LAST_IMPORTED_BLAZE_WORKSPACE;
+      case Bazel: return LAST_IMPORTED_BAZEL_WORKSPACE;
+      default: throw new RuntimeException("Unrecognized build system type: " + buildSystem);
+    }
+  }
+
+  private final BlazeWizardUserSettings userSettings;
+  private BlazeSelectWorkspaceOption workspaceOption;
+  private BlazeSelectProjectViewOption projectViewOption;
+  private File projectViewFile;
+  private ProjectView projectView;
+  private ProjectViewSet projectViewSet;
+  private String projectName;
+  private String projectDataDirectory;
+  private WorkspaceRoot workspaceRoot;
+  private BuildSystem buildSystem;
+
+  public BlazeNewProjectBuilder() {
+    this.userSettings = BlazeWizardUserSettingsStorage.getInstance().copyUserSettings();
+  }
+
+  public BlazeWizardUserSettings getUserSettings() {
+    return userSettings;
+  }
+
+  public BlazeSelectWorkspaceOption getWorkspaceOption() {
+    return workspaceOption;
+  }
+
+  public BlazeSelectProjectViewOption getProjectViewOption() {
+    return projectViewOption;
+  }
+
+  public String getProjectName() {
+    return projectName;
+  }
+
+  public ProjectView getProjectView() {
+    return projectView;
+  }
+
+  public ProjectViewSet getProjectViewSet() {
+    return projectViewSet;
+  }
+
+  public String getProjectDataDirectory() {
+    return projectDataDirectory;
+  }
+
+  public BuildSystem getBuildSystem() {
+    return buildSystem;
+  }
+
+  public String getBuildSystemName() {
+    if (buildSystem != null) {
+      return buildSystem.getName();
+    }
+    return Blaze.defaultBuildSystemName();
+  }
+
+  public BlazeNewProjectBuilder setWorkspaceOption(BlazeSelectWorkspaceOption workspaceOption) {
+    this.workspaceOption = workspaceOption;
+    this.buildSystem = workspaceOption.getBuildSystemForWorkspace();
+    return this;
+  }
+
+  public BlazeNewProjectBuilder setProjectViewOption(BlazeSelectProjectViewOption projectViewOption) {
+    this.projectViewOption = projectViewOption;
+    return this;
+  }
+
+  public BlazeNewProjectBuilder setProjectView(ProjectView projectView) {
+    this.projectView = projectView;
+    return this;
+  }
+
+  public BlazeNewProjectBuilder setProjectViewFile(File projectViewFile) {
+    this.projectViewFile = projectViewFile;
+    return this;
+  }
+
+  public BlazeNewProjectBuilder setProjectViewSet(ProjectViewSet projectViewSet) {
+    this.projectViewSet = projectViewSet;
+    return this;
+  }
+
+  public BlazeNewProjectBuilder setProjectName(String projectName) {
+    this.projectName = projectName;
+    return this;
+  }
+
+  public BlazeNewProjectBuilder setProjectDataDirectory(String projectDataDirectory) {
+    this.projectDataDirectory = projectDataDirectory;
+    return this;
+  }
+
+  /**
+   * Commits the project. May report errors.
+   */
+  public void commit() throws BlazeProjectCommitException {
+    this.workspaceRoot = workspaceOption.commit();
+    projectViewOption.commit();
+
+    String workspaceKey = lastImportedWorkspaceKey(workspaceOption.getBuildSystemForWorkspace());
+    userSettings.put(workspaceKey, workspaceRoot.toString());
+
+    if (!StringUtil.isEmpty(projectDataDirectory)) {
+      File projectDataDir = new File(projectDataDirectory);
+      if (!projectDataDir.exists()) {
+        if (!projectDataDir.mkdirs()) {
+          throw new BlazeProjectCommitException("Unable to create the project directory: " + projectDataDirectory);
+        }
+      }
+    }
+
+    try {
+      LOG.assertTrue(projectViewFile != null);
+      ProjectViewStorageManager.getInstance().writeProjectView(
+        ProjectViewParser.projectViewToString(projectView),
+        projectViewFile
+      );
+    } catch (IOException e) {
+      throw new BlazeProjectCommitException("Could not create project view file", e);
+    }
+  }
+
+  /**
+   * Commits the project data. This method mustn't fail, because the project
+   * has already been created.
+   */
+  public void commitToProject(Project project) {
+    BlazeWizardUserSettingsStorage.getInstance().commit(userSettings);
+
+    BlazeImportSettings importSettings = new BlazeImportSettings(
+      workspaceRoot.directory().getPath(),
+      projectName,
+      projectDataDirectory,
+      createLocationHash(projectName),
+      projectViewFile.getPath(),
+      buildSystem
+    );
+
+    BlazeImportSettingsManager.getInstance(project).setImportSettings(importSettings);
+    PluginDependencyHelper.addDependencyOnSyncPlugin(project);
+    // Initial sync of the project happens in BlazeSyncStartupActivity
+  }
+
+  private static String createLocationHash(String projectName) {
+    String uuid = UUID.randomUUID().toString();
+    uuid = uuid.substring(0, Math.min(uuid.length(), 8));
+    return projectName.replaceAll("[^a-zA-Z0-9]", "") + "-" + uuid;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeProjectCommitException.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeProjectCommitException.java
new file mode 100644
index 0000000..f52e94d
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeProjectCommitException.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.wizard2;
+
+/**
+ * Throws during the commit stage of the new project wizard.
+ */
+public class BlazeProjectCommitException extends Exception {
+  public BlazeProjectCommitException(String message) {
+    super(message);
+  }
+
+  public BlazeProjectCommitException(String message, Throwable cause) {
+    super(message, cause);
+  }
+
+  public BlazeProjectCommitException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.java
new file mode 100644
index 0000000..f5e24ce
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeSelectProjectViewOption.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.wizard2;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+
+import javax.annotation.Nullable;
+
+/**
+ * Provides an option on the "Select .blazeproject" screen
+ */
+public interface BlazeSelectProjectViewOption extends BlazeWizardOption {
+  @Nullable
+  WorkspacePath getSharedProjectView();
+
+  @Nullable
+  String getInitialProjectViewText();
+
+  void commit();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java
new file mode 100644
index 0000000..3a2a941
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeSelectWorkspaceOption.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.wizard2;
+
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+
+/**
+ * Provides an option on the "Select workspace" screen
+ */
+public interface BlazeSelectWorkspaceOption extends BlazeWizardOption {
+  /**
+   * @return a location to use when browsing for workspace paths.
+   */
+  WorkspaceRoot getTemporaryWorkspaceRoot();
+
+  /**
+   * @return the name of the workspace. Used to generate default project names.
+   */
+  String getWorkspaceName();
+
+  BuildSystem getBuildSystemForWorkspace();
+
+  WorkspaceRoot commit() throws BlazeProjectCommitException;
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOption.java
new file mode 100644
index 0000000..110804e
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOption.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.base.wizard2;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+/**
+ * Base class for the workspace and project view options.
+ */
+public interface BlazeWizardOption {
+  int HEADER_LABEL_WIDTH = 80;
+  int MAX_INPUT_FIELD_WIDTH = 600;
+
+  /**
+   * @return A stable option name, used to remember which option was selected.
+   */
+  String getOptionName();
+
+  /**
+   * @return the option text, eg "Create workspace from scratch"
+   */
+  String getOptionText();
+
+  /**
+   * @return a ui component to be added below the corresponding radio button
+   */
+  @Nullable
+  JComponent getUiComponent();
+
+  BlazeValidationResult validate();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java
new file mode 100644
index 0000000..b928774
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardOptionProvider.java
@@ -0,0 +1,32 @@
+/*
+ * 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.intellij.openapi.extensions.ExtensionPointName;
+
+import java.util.Collection;
+
+/**
+ * Provides options during the import process.
+ */
+public interface BlazeWizardOptionProvider {
+  ExtensionPointName<BlazeWizardOptionProvider> EP_NAME =
+    ExtensionPointName.create("com.google.idea.blaze.BlazeWizardOptionProvider");
+
+  Collection<BlazeSelectWorkspaceOption> getSelectWorkspaceOptions(BlazeNewProjectBuilder builder);
+
+  Collection<BlazeSelectProjectViewOption> getSelectProjectViewOptions(BlazeNewProjectBuilder builder);
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettings.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettings.java
new file mode 100644
index 0000000..bafe536
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettings.java
@@ -0,0 +1,72 @@
+/*
+ * 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.Objects;
+import com.google.common.collect.Maps;
+import com.intellij.util.xmlb.annotations.MapAnnotation;
+import com.intellij.util.xmlb.annotations.Tag;
+
+import java.util.Map;
+
+/**
+ * A bundle of settings that are stored between invocations of the wizard.
+ *
+ * <p>It's the user's responsibility to appropriately namespace the keys.
+ */
+public class BlazeWizardUserSettings {
+  Map<String, String> values = Maps.newHashMap();
+
+  public BlazeWizardUserSettings() {
+  }
+
+  public BlazeWizardUserSettings(BlazeWizardUserSettings state) {
+    values.putAll(state.getValues());
+  }
+
+  public String get(String key, String defaultValue) {
+    return values.getOrDefault(key, defaultValue);
+  }
+
+  public void put(String key, String value) {
+    values.put(key, value);
+  }
+
+  @SuppressWarnings("unused")
+  @Tag("settings")
+  @MapAnnotation(surroundWithTag = false)
+  public Map<String, String> getValues() {
+    return values;
+  }
+
+  @SuppressWarnings("unused")
+  public void setValues(Map<String, String> values) {
+    this.values = values;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    BlazeWizardUserSettings that = (BlazeWizardUserSettings)o;
+    return Objects.equal(values, that.values);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(values);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java
new file mode 100644
index 0000000..9492476
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/BlazeWizardUserSettingsStorage.java
@@ -0,0 +1,56 @@
+/*
+ * 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.intellij.openapi.components.PersistentStateComponent;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.components.State;
+import com.intellij.openapi.components.Storage;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Stores wizard user settings between runs.
+ */
+@State(
+  name = "BlazeWizardUserSettings",
+  storages = @Storage(value = "blaze.wizard.settings.xml")
+)
+public class BlazeWizardUserSettingsStorage implements PersistentStateComponent<BlazeWizardUserSettings> {
+  private BlazeWizardUserSettings state = new BlazeWizardUserSettings();
+
+  static BlazeWizardUserSettingsStorage getInstance() {
+    return ServiceManager.getService(BlazeWizardUserSettingsStorage.class);
+  }
+
+  @Nullable
+  @Override
+  public BlazeWizardUserSettings getState() {
+    return state;
+  }
+
+  @Override
+  public void loadState(BlazeWizardUserSettings state) {
+    this.state = state;
+  }
+
+  BlazeWizardUserSettings copyUserSettings() {
+    return new BlazeWizardUserSettings(state);
+  }
+
+  void commit(BlazeWizardUserSettings userSettings) {
+    this.state = userSettings;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
new file mode 100644
index 0000000..8d05996
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/CopyExternalProjectViewOption.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.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;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.fileChooser.FileChooserDialog;
+import com.intellij.openapi.fileChooser.FileChooserFactory;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.components.panels.HorizontalLayout;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+class CopyExternalProjectViewOption implements BlazeSelectProjectViewOption {
+  private static final String LAST_WORKSPACE_PATH = "copy-external.last-project-view-path";
+
+  final BlazeWizardUserSettings userSettings;
+  final JComponent component;
+  final JTextField projectViewPathField;
+
+  CopyExternalProjectViewOption(BlazeNewProjectBuilder builder) {
+    this.userSettings = builder.getUserSettings();
+
+    String defaultWorkspacePath = userSettings.get(LAST_WORKSPACE_PATH, "");
+
+    JPanel panel = new JPanel(new HorizontalLayout(10));
+    JLabel label = new JLabel("Project view:");
+    UiUtil.setPreferredWidth(label, HEADER_LABEL_WIDTH);
+    panel.add(label);
+    this.projectViewPathField = new JTextField();
+    projectViewPathField.setText(defaultWorkspacePath);
+    UiUtil.setPreferredWidth(projectViewPathField, MAX_INPUT_FIELD_WIDTH);
+    panel.add(projectViewPathField);
+    JButton button = new JButton("...");
+    button.addActionListener(action -> chooseWorkspacePath());
+    int buttonSize = projectViewPathField.getPreferredSize().height;
+    button.setPreferredSize(new Dimension(buttonSize, buttonSize));
+    panel.add(button);
+    this.component = panel;
+  }
+
+  @Override
+  public String getOptionName() {
+    return "copy-external";
+  }
+
+  @Override
+  public String getOptionText() {
+    return "Copy external";
+  }
+
+  @Override
+  public JComponent getUiComponent() {
+    return component;
+  }
+
+  @Override
+  public BlazeValidationResult validate() {
+    if (getProjectViewPath().isEmpty()) {
+      return BlazeValidationResult.failure("Path to project view file cannot be empty.");
+    }
+    File file = new File(getProjectViewPath());
+    if (!file.exists()) {
+      return BlazeValidationResult.failure("Project view file does not exist.");
+    }
+    return BlazeValidationResult.success();
+  }
+
+  @Nullable
+  @Override
+  public WorkspacePath getSharedProjectView() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getInitialProjectViewText() {
+    try {
+      byte[] bytes = Files.readAllBytes(Paths.get(getProjectViewPath()));
+      return new String(bytes, StandardCharsets.UTF_8);
+    }
+    catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Override
+  public void commit() {
+    userSettings.put(LAST_WORKSPACE_PATH, getProjectViewPath());
+  }
+
+  private String getProjectViewPath() {
+    return projectViewPathField.getText().trim();
+  }
+
+  private void chooseWorkspacePath() {
+    FileChooserDescriptor descriptor = new FileChooserDescriptor(true, false, false, false, false, false)
+      .withShowHiddenFiles(true) // Show root project view file
+      .withHideIgnored(false)
+      .withTitle("Select Project View File")
+      .withDescription("Select a project view file to import.")
+      .withFileFilter(virtualFile -> ProjectViewStorageManager.isProjectViewFile(new File(virtualFile.getPath())));
+    FileChooserDialog chooser = FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
+
+    File startingLocation = null;
+    String projectViewPath = getProjectViewPath();
+    if (!projectViewPath.isEmpty()) {
+      File fileLocation = new File(projectViewPath);
+      if (fileLocation.exists()) {
+        startingLocation = fileLocation;
+      }
+    }
+    final VirtualFile[] files;
+    if (startingLocation != null) {
+      VirtualFile toSelect = LocalFileSystem.getInstance().refreshAndFindFileByPath(startingLocation.getPath());
+      files = chooser.choose(null, toSelect);
+    } else {
+      files = chooser.choose(null);
+    }
+    if (files.length == 0) {
+      return;
+    }
+    VirtualFile file = files[0];
+    projectViewPathField.setText(file.getPath());
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java
new file mode 100644
index 0000000..89cbb32
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/CreateFromScratchProjectViewOption.java
@@ -0,0 +1,60 @@
+/*
+ * 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.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+
+class CreateFromScratchProjectViewOption implements BlazeSelectProjectViewOption {
+  @Override
+  public String getOptionName() {
+    return "create-from-scratch";
+  }
+
+  @Override
+  public String getOptionText() {
+    return "Create from scratch";
+  }
+
+  @Override
+  public JComponent getUiComponent() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public WorkspacePath getSharedProjectView() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getInitialProjectViewText() {
+    return "";
+  }
+
+  @Override
+  public void commit() {
+  }
+
+  @Override
+  public BlazeValidationResult validate() {
+    return BlazeValidationResult.success();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
new file mode 100644
index 0000000..1c2c41c
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/GenerateFromBuildFileSelectProjectViewOption.java
@@ -0,0 +1,202 @@
+/*
+ * 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.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.ProjectView;
+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.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.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.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.components.panels.HorizontalLayout;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+
+class GenerateFromBuildFileSelectProjectViewOption implements BlazeSelectProjectViewOption {
+  private static final String LAST_WORKSPACE_PATH = "generate-from-build-file.last-workspace-path";
+  private final BlazeNewProjectBuilder builder;
+  private final BlazeWizardUserSettings userSettings;
+  private final JTextField buildFilePathField;
+  private final JPanel component;
+
+  public GenerateFromBuildFileSelectProjectViewOption(
+    BlazeNewProjectBuilder builder) {
+    this.builder = builder;
+    this.userSettings = builder.getUserSettings();
+
+    String defaultWorkspacePath = userSettings.get(LAST_WORKSPACE_PATH, "");
+
+    JPanel panel = new JPanel(new HorizontalLayout(10));
+    JLabel pathLabel = new JLabel("BUILD file:");
+    UiUtil.setPreferredWidth(pathLabel, HEADER_LABEL_WIDTH);
+    panel.add(pathLabel);
+    this.buildFilePathField = new JTextField();
+    buildFilePathField.setText(defaultWorkspacePath);
+    UiUtil.setPreferredWidth(buildFilePathField, MAX_INPUT_FIELD_WIDTH);
+    panel.add(buildFilePathField);
+    JButton button = new JButton("...");
+    button.addActionListener(action -> chooseWorkspacePath());
+
+    int buttonSize = buildFilePathField.getPreferredSize().height;
+    button.setPreferredSize(new Dimension(buttonSize, buttonSize));
+    panel.add(button);
+    this.component = panel;
+  }
+
+  @Override
+  public String getOptionName() {
+    return "generate-from-build-file";
+  }
+
+  @Override
+  public String getOptionText() {
+    return "Generate from BUILD file";
+  }
+
+  @Override
+  public JComponent getUiComponent() {
+    return component;
+  }
+
+  @Override
+  public BlazeValidationResult validate() {
+    if (getBuildFilePath().isEmpty()) {
+      return BlazeValidationResult.failure("BUILD file field cannot be empty.");
+    }
+    WorkspaceRoot workspaceRoot = builder.getWorkspaceOption().getTemporaryWorkspaceRoot();
+    File file = workspaceRoot.fileForPath(new WorkspacePath(getBuildFilePath()));
+    if (!file.exists()) {
+      return BlazeValidationResult.failure("BUILD file does not exist.");
+    }
+
+    return BlazeValidationResult.success();
+  }
+
+  @Nullable
+  @Override
+  public WorkspacePath getSharedProjectView() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public String getInitialProjectViewText() {
+    WorkspaceRoot workspaceRoot = builder.getWorkspaceOption().getTemporaryWorkspaceRoot();
+    WorkspacePath workspacePath = new WorkspacePath(getBuildFilePath());
+    return guessProjectViewFromLocation(workspaceRoot,
+                                        workspaceRoot.workspacePathFor(workspaceRoot.fileForPath(workspacePath).getParentFile()));
+  }
+
+  @Override
+  public void commit() {
+    userSettings.put(LAST_WORKSPACE_PATH, getBuildFilePath());
+  }
+
+  private static String guessProjectViewFromLocation(WorkspaceRoot workspaceRoot, WorkspacePath workspacePath) {
+
+    WorkspacePath mainModuleWorkspaceRelativePath = workspacePath;
+    WorkspacePath testModuleWorkspaceRelativePath = guessTestRelativePath(
+      workspaceRoot,
+      mainModuleWorkspaceRelativePath);
+
+    ListSection.Builder<DirectoryEntry> directorySectionBuilder = ListSection.builder(DirectorySection.KEY);
+    directorySectionBuilder.add(DirectoryEntry.include(mainModuleWorkspaceRelativePath));
+    if (testModuleWorkspaceRelativePath != null) {
+      directorySectionBuilder.add(DirectoryEntry.include(testModuleWorkspaceRelativePath));
+    }
+
+    ListSection.Builder<TargetExpression> targetSectionBuilder = ListSection.builder(TargetSection.KEY);
+    targetSectionBuilder.add(TargetExpression.allFromPackageRecursive(mainModuleWorkspaceRelativePath));
+    if (testModuleWorkspaceRelativePath != null) {
+      targetSectionBuilder.add(TargetExpression.allFromPackageRecursive(testModuleWorkspaceRelativePath));
+    }
+
+    return ProjectViewParser.projectViewToString(
+      ProjectView.builder()
+        .put(directorySectionBuilder)
+        .put(targetSectionBuilder)
+        .build()
+    );
+  }
+
+  @Nullable
+  private static WorkspacePath guessTestRelativePath(
+    WorkspaceRoot workspaceRoot,
+    WorkspacePath projectWorkspacePath) {
+    String projectRelativePath = projectWorkspacePath.relativePath();
+    String testBuildFileRelativePath = null;
+    if (projectRelativePath.startsWith("java/")) {
+      testBuildFileRelativePath = projectRelativePath.replaceFirst("java/", "javatests/");
+    }
+    else if (projectRelativePath.contains("/java/")) {
+      testBuildFileRelativePath = projectRelativePath.replaceFirst("/java/", "/javatests/");
+    }
+    if (testBuildFileRelativePath != null) {
+      File testBuildFile = workspaceRoot.fileForPath(new WorkspacePath(testBuildFileRelativePath));
+      if (testBuildFile.exists()) {
+        return new WorkspacePath(testBuildFileRelativePath);
+      }
+    }
+    return null;
+  }
+
+  private String getBuildFilePath() {
+    return buildFilePathField.getText().trim();
+  }
+
+  private void chooseWorkspacePath() {
+    FileChooserDescriptor descriptor = new FileChooserDescriptor(true, false, false, false, false, false)
+      .withShowHiddenFiles(true) // Show root project view file
+      .withHideIgnored(false)
+      .withTitle("Select BUILD File")
+      .withDescription("Select a BUILD file to synthesize a project view from.")
+      .withFileFilter(virtualFile -> virtualFile.getName().equals("BUILD"));
+    FileChooserDialog chooser = FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
+
+    WorkspaceRoot workspaceRoot = builder.getWorkspaceOption().getTemporaryWorkspaceRoot();
+
+    File startingLocation = workspaceRoot.directory();
+    String buildFilePath = getBuildFilePath();
+    if (!buildFilePath.isEmpty()) {
+      File fileLocation = workspaceRoot.fileForPath(new WorkspacePath(buildFilePath));
+      if (fileLocation.exists()) {
+        startingLocation = fileLocation;
+      }
+    }
+    VirtualFile toSelect = LocalFileSystem.getInstance().refreshAndFindFileByPath(startingLocation.getPath());
+    VirtualFile[] files = chooser.choose(null, toSelect);
+    if (files.length == 0) {
+      return;
+    }
+    VirtualFile file = files[0];
+    String newWorkspacePath = FileUtil.getRelativePath(workspaceRoot.directory(), new File(file.getPath()));
+    buildFilePathField.setText(newWorkspacePath);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
new file mode 100644
index 0000000..3fa8388
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/ImportFromWorkspaceProjectViewOption.java
@@ -0,0 +1,157 @@
+/*
+ * 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.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+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.ui.Messages;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.components.panels.HorizontalLayout;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+
+class ImportFromWorkspaceProjectViewOption implements BlazeSelectProjectViewOption {
+  private static final String LAST_WORKSPACE_PATH = "import-from-workspace.last-workspace-path";
+
+  final BlazeNewProjectBuilder builder;
+  final BlazeWizardUserSettings userSettings;
+  final JComponent component;
+  final JTextField projectViewPathField;
+
+  ImportFromWorkspaceProjectViewOption(BlazeNewProjectBuilder builder) {
+    this.builder = builder;
+    this.userSettings = builder.getUserSettings();
+
+    String defaultWorkspacePath = userSettings.get(LAST_WORKSPACE_PATH, "");
+
+    JPanel panel = new JPanel(new HorizontalLayout(10));
+    JLabel projectViewLabel = new JLabel("Project view:");
+    UiUtil.setPreferredWidth(projectViewLabel, HEADER_LABEL_WIDTH);
+    panel.add(projectViewLabel);
+    this.projectViewPathField = new JTextField();
+    projectViewPathField.setText(defaultWorkspacePath);
+    UiUtil.setPreferredWidth(projectViewPathField, MAX_INPUT_FIELD_WIDTH);
+    panel.add(projectViewPathField);
+    JButton button = new JButton("...");
+    button.addActionListener(action -> chooseWorkspacePath());
+    int buttonSize = projectViewPathField.getPreferredSize().height;
+    button.setPreferredSize(new Dimension(buttonSize, buttonSize));
+    panel.add(button);
+    this.component = panel;
+  }
+
+  @Override
+  public String getOptionName() {
+    return "import-from-workspace";
+  }
+
+  @Override
+  public String getOptionText() {
+    return "Import from workspace";
+  }
+
+  @Override
+  public JComponent getUiComponent() {
+    return component;
+  }
+
+  @Override
+  public BlazeValidationResult validate() {
+    if (getProjectViewPath().isEmpty()) {
+      return BlazeValidationResult.failure("Workspace path to project view file cannot be empty.");
+    }
+    WorkspaceRoot workspaceRoot = builder.getWorkspaceOption().getTemporaryWorkspaceRoot();
+    File file = workspaceRoot.fileForPath(getSharedProjectView());
+    if (!file.exists()) {
+      return BlazeValidationResult.failure("Project view file does not exist.");
+    }
+
+    return BlazeValidationResult.success();
+  }
+
+  @Nullable
+  @Override
+  public WorkspacePath getSharedProjectView() {
+    return new WorkspacePath(getProjectViewPath());
+  }
+
+  @Nullable
+  @Override
+  public String getInitialProjectViewText() {
+    return null;
+  }
+
+  @Override
+  public void commit() {
+    userSettings.put(LAST_WORKSPACE_PATH, getProjectViewPath());
+  }
+
+  private String getProjectViewPath() {
+    return projectViewPathField.getText().trim();
+  }
+
+  private void chooseWorkspacePath() {
+    FileChooserDescriptor descriptor = new FileChooserDescriptor(true, false, false, false, false, false)
+      .withShowHiddenFiles(true) // Show root project view file
+      .withHideIgnored(false)
+      .withTitle("Select Project View File")
+      .withDescription("Select a project view file to import.")
+      .withFileFilter(virtualFile -> ProjectViewStorageManager.isProjectViewFile(new File(virtualFile.getPath())));
+    FileChooserDialog chooser = FileChooserFactory.getInstance().createFileChooser(descriptor, null, null);
+
+    WorkspaceRoot workspaceRoot = builder.getWorkspaceOption().getTemporaryWorkspaceRoot();
+
+    File startingLocation = workspaceRoot.directory();
+    String projectViewPath = getProjectViewPath();
+    if (!projectViewPath.isEmpty()) {
+      File fileLocation = workspaceRoot.fileForPath(new WorkspacePath(projectViewPath));
+      if (fileLocation.exists()) {
+        startingLocation = fileLocation;
+      }
+    }
+    VirtualFile toSelect = LocalFileSystem.getInstance().refreshAndFindFileByPath(startingLocation.getPath());
+    VirtualFile[] files = chooser.choose(null, toSelect);
+    if (files.length == 0) {
+      return;
+    }
+    VirtualFile file = files[0];
+
+    if (!FileUtil.isAncestor(workspaceRoot.directory().getPath(), file.getPath(), true)) {
+      Messages.showErrorDialog(
+        String.format(
+          "You must choose a project view file under %s. To use an external project view, please use the 'Copy external' option.",
+          workspaceRoot.directory().getPath()
+        ),
+        "Cannot Use Project View File"
+      );
+      return;
+    }
+
+    String newWorkspacePath = FileUtil.getRelativePath(workspaceRoot.directory(), new File(file.getPath()));
+    projectViewPathField.setText(newWorkspacePath);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/README b/blaze-base/src/com/google/idea/blaze/base/wizard2/README
new file mode 100644
index 0000000..1dc0c66
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/README
@@ -0,0 +1,5 @@
+This package has the reusable components for the wizard process for
+creating a new project.
+
+Each IDE (eg. IntelliJ v. CLion) has its own project creation framework, so
+for each IDE we have to implement an adapter between that and these classes.
\ No newline at end of file
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
new file mode 100644
index 0000000..a19feb3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/UseExistingBazelWorkspaceOption.java
@@ -0,0 +1,60 @@
+/*
+ * 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.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.intellij.openapi.vfs.VirtualFile;
+import icons.BlazeIcons;
+
+import javax.swing.*;
+import java.io.File;
+
+class UseExistingBazelWorkspaceOption extends UseExistingWorkspaceOption {
+
+  UseExistingBazelWorkspaceOption(BlazeNewProjectBuilder builder) {
+    super(builder, BuildSystem.Bazel);
+  }
+
+  @Override
+  protected boolean isWorkspaceRoot(VirtualFile file) {
+    return BuildSystemProvider.getWorkspaceRootProvider(BuildSystem.Bazel).isWorkspaceRoot(new File(file.getPath()));
+  }
+
+  @Override
+  public String getOptionName() {
+    return "use-existing-bazel-workspace";
+  }
+  @Override
+  public String getOptionText() {
+    return "Use existing bazel workspace";
+  }
+
+  @Override
+  protected String getWorkspaceName(File workspaceRoot) {
+    return workspaceRoot.getName();
+  }
+
+  @Override
+  protected String fileChooserDescription() {
+    return "Select the directory of the workspace you want to use.";
+  }
+
+  @Override
+  protected Icon getBuildSystemIcon() {
+    return BlazeIcons.BazelLeaf;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java
new file mode 100644
index 0000000..79875aa
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/UseExistingWorkspaceOption.java
@@ -0,0 +1,170 @@
+/*
+ * 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.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.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.components.panels.HorizontalLayout;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+
+public abstract class UseExistingWorkspaceOption implements BlazeSelectWorkspaceOption {
+
+  private final BlazeWizardUserSettings userSettings;
+  private final JComponent component;
+  private final JTextField directoryField;
+  private final BuildSystem buildSystem;
+
+  protected UseExistingWorkspaceOption(BlazeNewProjectBuilder builder, BuildSystem buildSystem) {
+    this.userSettings = builder.getUserSettings();
+    this.buildSystem = buildSystem;
+
+    String defaultDirectory = userSettings.get(BlazeNewProjectBuilder.lastImportedWorkspaceKey(buildSystem), "");
+
+    JPanel panel = new JPanel(new HorizontalLayout(10));
+    panel.add(getIconComponent());
+    JLabel workspaceRootLabel = new JLabel("Workspace:");
+    UiUtil.setPreferredWidth(workspaceRootLabel, HEADER_LABEL_WIDTH);
+    panel.add(workspaceRootLabel);
+    this.directoryField = new JTextField();
+    directoryField.setText(defaultDirectory);
+    UiUtil.setPreferredWidth(directoryField, MAX_INPUT_FIELD_WIDTH);
+    panel.add(directoryField);
+    JButton button = new JButton("...");
+    button.addActionListener(action -> chooseDirectory());
+    int buttonSize = directoryField.getPreferredSize().height;
+    button.setPreferredSize(new Dimension(buttonSize, buttonSize));
+    panel.add(button);
+    this.component = panel;
+  }
+
+  protected abstract boolean isWorkspaceRoot(VirtualFile file);
+
+  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 WorkspaceRoot commit() throws BlazeProjectCommitException {
+    return new WorkspaceRoot(new File(getDirectory()));
+  }
+
+  @Nullable
+  @Override
+  public WorkspaceRoot getTemporaryWorkspaceRoot() {
+    return new WorkspaceRoot(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 workspaceRoot = new File(getDirectory());
+    if (!workspaceRoot.exists()) {
+      return BlazeValidationResult.failure("Workspace does not exist");
+    }
+    return BlazeValidationResult.success();
+  }
+
+  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 Component getIconComponent() {
+    JLabel iconPanel = new JLabel(IconLoader.getIconSnapshot(getBuildSystemIcon())) {
+      @Override
+      public boolean isEnabled() {
+        return true;
+      }
+    };
+    UiUtil.setPreferredWidth(iconPanel, 16);
+    return iconPanel;
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
new file mode 100644
index 0000000..a4d41d3
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeEditProjectViewControl.java
@@ -0,0 +1,303 @@
+/*
+ * 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.ui;
+
+import com.google.common.collect.Lists;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hashing;
+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.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.ImportSection;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.ui.JPanelProvidingProject;
+import com.google.idea.blaze.base.settings.ui.ProjectViewUi;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.BlazeSelectProjectViewOption;
+import com.google.idea.blaze.base.wizard2.BlazeSelectWorkspaceOption;
+import com.intellij.ide.RecentProjectsManager;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationNamesInfo;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.fileChooser.FileChooserDescriptor;
+import com.intellij.openapi.ui.TextComponentAccessor;
+import com.intellij.openapi.ui.TextFieldWithBrowseButton;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.ui.components.JBLabel;
+import com.intellij.util.SystemProperties;
+import org.jetbrains.annotations.Nullable;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * The UI control to collect project settings when importing a Blaze project.
+ */
+public final class BlazeEditProjectViewControl {
+
+  private static final FileChooserDescriptor PROJECT_FOLDER_DESCRIPTOR =
+    new FileChooserDescriptor(false, true, false, false, false, false);
+  private static final Logger LOG = Logger.getInstance(BlazeEditProjectViewControl.class);
+
+  private final JPanel component;
+  private final String buildSystemName;
+  private final ProjectViewUi projectViewUi;
+
+  private TextFieldWithBrowseButton projectDataDirField;
+  private JTextField projectNameField;
+  private HashCode paramsHash;
+
+  public BlazeEditProjectViewControl(BlazeNewProjectBuilder builder, Disposable parentDisposable) {
+    this.projectViewUi = new ProjectViewUi(parentDisposable);
+    JPanel component = new JPanelProvidingProject(ProjectViewUi.getProject(), new GridBagLayout());
+    fillUi(component, 0, parentDisposable);
+    update(builder);
+    UiUtil.fillBottom(component);
+    this.component = component;
+    this.buildSystemName = builder.getBuildSystemName();
+  }
+
+  public Component getUiComponent() {
+    return component;
+  }
+
+  private void fillUi(JPanel canvas, int indentLevel, Disposable parentDisposable) {
+    JLabel projectDataDirLabel = new JBLabel("Project data directory:");
+
+    Dimension minSize = ProjectViewUi.getMinimumSize();
+    // Add 120 pixels so we have room for our extra fields
+    minSize.setSize(minSize.width, minSize.height + 120);
+    canvas.setMinimumSize(minSize);
+    canvas.setPreferredSize(minSize);
+
+    projectDataDirField = new TextFieldWithBrowseButton();
+    projectDataDirField.addBrowseFolderListener("", buildSystemName + " project data directory", null,
+                                                PROJECT_FOLDER_DESCRIPTOR,
+                                                TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT, false);
+    final String dataDirToolTipText =
+      "Directory in which to store the project's metadata. Choose a directory outside of your workspace.";
+    projectDataDirField.setToolTipText(dataDirToolTipText);
+    projectDataDirLabel.setToolTipText(dataDirToolTipText);
+
+    canvas.add(projectDataDirLabel, UiUtil.getLabelConstraints(indentLevel));
+    canvas.add(projectDataDirField, UiUtil.getFillLineConstraints(0));
+
+    JLabel projectNameLabel = new JLabel("Project name:");
+    projectNameField = new JTextField();
+    final String projectNameToolTipText =
+      "Project display name.";
+    projectNameField.setToolTipText(projectNameToolTipText);
+    projectNameLabel.setToolTipText(projectNameToolTipText);
+    canvas.add(projectNameLabel, UiUtil.getLabelConstraints(indentLevel));
+    canvas.add(projectNameField, UiUtil.getFillLineConstraints(0));
+
+    projectViewUi.fillUi(canvas, indentLevel);
+  }
+
+  public void update(BlazeNewProjectBuilder builder) {
+    BlazeSelectWorkspaceOption workspaceOption = builder.getWorkspaceOption();
+    BlazeSelectProjectViewOption projectViewOption = builder.getProjectViewOption();
+    String workspaceName = workspaceOption.getWorkspaceName();
+    WorkspaceRoot workspaceRoot = workspaceOption.getTemporaryWorkspaceRoot();
+    WorkspacePath workspacePath = projectViewOption.getSharedProjectView();
+    String initialProjectViewText = projectViewOption.getInitialProjectViewText();
+
+    HashCode hashCode = Hashing.md5().newHasher()
+      .putUnencodedChars(workspaceName)
+      .putUnencodedChars(workspaceRoot.toString())
+      .putUnencodedChars(workspacePath != null ? workspacePath.toString() : "")
+      .putUnencodedChars(initialProjectViewText != null ? initialProjectViewText : "")
+      .hash();
+
+    // If any params have changed, reinit the control
+    if (!hashCode.equals(paramsHash)) {
+      this.paramsHash = hashCode;
+      init(workspaceName, workspaceRoot, workspacePath, initialProjectViewText);
+    }
+  }
+
+  private void init(String workspaceName,
+                    WorkspaceRoot workspaceRoot,
+                    @Nullable WorkspacePath sharedProjectView,
+                    @Nullable String initialProjectViewText) {
+
+    projectNameField.setText(workspaceName);
+    String defaultDataDir = getDefaultProjectDataDirectory(workspaceName);
+    projectDataDirField.setText(defaultDataDir);
+
+    String projectViewText = "";
+    File sharedProjectViewFile = null;
+
+    if (sharedProjectView != null) {
+      sharedProjectViewFile = workspaceRoot.fileForPath(sharedProjectView);
+
+      try {
+        projectViewText = ProjectViewStorageManager.getInstance().loadProjectView(sharedProjectViewFile);
+        if (projectViewText == null) {
+          LOG.error("Could not load project view: " + sharedProjectViewFile);
+          projectViewText = "";
+        }
+      }
+      catch (IOException e) {
+        LOG.error(e);
+      }
+    } else {
+      projectViewText = initialProjectViewText;
+      LOG.assertTrue(projectViewText != null);
+    }
+
+    projectViewUi.init(
+      workspaceRoot,
+      projectViewText,
+      sharedProjectViewFile != null ? projectViewText : null,
+      sharedProjectViewFile,
+      sharedProjectViewFile != null,
+      false /* allowEditShared - not allowed during import */
+    );
+  }
+
+  private static String getDefaultProjectDataDirectory(String projectName) {
+    File defaultDataDirectory = new File(getDefaultProjectsDirectory());
+    File desiredLocation = new File(defaultDataDirectory, projectName);
+    return newUniquePath(desiredLocation);
+  }
+
+  private static String getDefaultProjectsDirectory() {
+    final String lastProjectLocation = RecentProjectsManager.getInstance().getLastProjectCreationLocation();
+    if (lastProjectLocation != null) {
+      return lastProjectLocation.replace('/', File.separatorChar);
+    }
+    final String userHome = SystemProperties.getUserHome();
+    String productName = ApplicationNamesInfo.getInstance().getLowercaseProductName();
+    return userHome.replace('/', File.separatorChar) + File.separator + productName.replace(" ", "") + "Projects";
+  }
+
+  /**
+   * Returns a unique file path by appending numbers until a non-collision is found.
+   */
+  private static String newUniquePath(File location) {
+    if (!location.exists()) {
+      return location.getAbsolutePath();
+    }
+
+    String name = location.getName();
+    File directory = location.getParentFile();
+    int tries = 0;
+    while (true) {
+      String candidateName = String.format("%s-%02d", name, tries);
+      File candidateFile = new File(directory, candidateName);
+      if (!candidateFile.exists()) {
+        return candidateFile.getAbsolutePath();
+      }
+      tries++;
+    }
+  }
+
+  public BlazeValidationResult validate() {
+    // Validate project settings fields
+    String projectName = projectNameField.getText().trim();
+    if (StringUtil.isEmpty(projectName)) {
+      return BlazeValidationResult.failure(new BlazeValidationError("Project name is not specified"));
+    }
+    String projectDataDirPath = projectDataDirField.getText().trim();
+    if (StringUtil.isEmpty(projectDataDirPath)) {
+      return BlazeValidationResult.failure(new BlazeValidationError("Project data directory is not specified"));
+    }
+    File projectDataDir = new File(projectDataDirPath);
+    if (!projectDataDir.isAbsolute()) {
+      return BlazeValidationResult.failure(new BlazeValidationError("Project data directory is not valid"));
+    }
+
+    List<IssueOutput> issues = Lists.newArrayList();
+
+    projectViewUi.parseProjectView(issues);
+    BlazeValidationError projectViewParseError = validationErrorFromIssueList(issues);
+    if (projectViewParseError != null) {
+      return BlazeValidationResult.failure(projectViewParseError);
+    }
+
+    return BlazeValidationResult.success();
+  }
+
+  @Nullable
+  private static BlazeValidationError validationErrorFromIssueList(List<IssueOutput> issues) {
+    List<IssueOutput> errors = issues
+      .stream()
+      .filter(issue -> issue.getCategory() == IssueOutput.Category.ERROR)
+      .collect(Collectors.toList());
+
+    if (!errors.isEmpty()) {
+      StringBuilder errorMessage = new StringBuilder();
+      errorMessage.append("The following issues were found:\n\n");
+      for (IssueOutput issue : errors) {
+        errorMessage.append(issue.getMessage());
+        errorMessage.append('\n');
+      }
+      return new BlazeValidationError(errorMessage.toString());
+    }
+    return null;
+  }
+
+  public void updateBuilder(BlazeNewProjectBuilder builder) {
+    String projectName = projectNameField.getText().trim();
+    String projectDataDirectory = projectDataDirField.getText().trim();
+    File localProjectViewFile = ProjectViewStorageManager.getLocalProjectViewFileName(
+      builder.getBuildSystem(), new File(projectDataDirectory)
+    );
+
+    BlazeSelectProjectViewOption selectProjectViewOption = builder.getProjectViewOption();
+    boolean useSharedProjectView = projectViewUi.getUseSharedProjectView();
+
+
+    // If we're using a shared project view, synthesize a local one that imports the shared one
+    ProjectViewSet parseResult = projectViewUi.parseProjectView(Lists.newArrayList());
+
+    final ProjectView projectView;
+    final ProjectViewSet projectViewSet;
+    if (useSharedProjectView && selectProjectViewOption.getSharedProjectView() != null) {
+      projectView = ProjectView.builder()
+        .put(ScalarSection.builder(ImportSection.KEY)
+               .set(selectProjectViewOption.getSharedProjectView()))
+        .build();
+      projectViewSet = ProjectViewSet.builder()
+        .addAll(parseResult.getProjectViewFiles())
+        .add(localProjectViewFile, projectView)
+        .build();
+    } else {
+      ProjectViewSet.ProjectViewFile projectViewFile = parseResult.getTopLevelProjectViewFile();
+      assert projectViewFile != null;
+      projectView = projectViewFile.projectView;
+      projectViewSet = parseResult;
+    }
+
+    builder
+      .setProjectView(projectView)
+      .setProjectViewFile(localProjectViewFile)
+      .setProjectViewSet(projectViewSet)
+      .setProjectName(projectName)
+      .setProjectDataDirectory(projectDataDirectory);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectOptionControl.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectOptionControl.java
new file mode 100644
index 0000000..8a5a8d6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectOptionControl.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.blaze.base.wizard2.ui;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.settings.ui.ProjectViewUi;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.BlazeWizardOption;
+import com.google.idea.blaze.base.wizard2.BlazeWizardUserSettings;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.ui.components.panels.HorizontalLayout;
+import com.intellij.ui.components.panels.VerticalLayout;
+
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import java.awt.*;
+import java.util.Collection;
+
+
+/**
+ * UI for selecting a client during the import process.
+ */
+public abstract class BlazeSelectOptionControl<T extends BlazeWizardOption> {
+  private static final Logger LOG = Logger.getInstance(BlazeSelectOptionControl.class);
+
+  private final BlazeWizardUserSettings userSettings;
+  private final JPanel canvas;
+  private final JLabel titleLabel;
+  private final Collection<OptionUiEntry<T>> optionUiEntryList;
+
+  static class OptionUiEntry<T> {
+    final T option;
+    final JRadioButton radioButton;
+    OptionUiEntry(T option, JRadioButton radioButton) {
+      this.option = option;
+      this.radioButton = radioButton;
+    }
+  }
+
+  BlazeSelectOptionControl(BlazeNewProjectBuilder builder,
+                           Collection<T> options) {
+    if (options == null) {
+      LOG.error("No options on select screen '" + getTitle() + "'");
+    }
+
+    this.userSettings = builder.getUserSettings();
+
+    JPanel canvas = new JPanel(new VerticalLayout(4));
+
+    Dimension minSize = ProjectViewUi.getMinimumSize();
+    canvas.setPreferredSize(minSize);
+
+    titleLabel = new JLabel(getTitle());
+    canvas.add(titleLabel);
+    canvas.add(new JSeparator());
+
+    JPanel content = new JPanel(new VerticalLayout(12));
+    content.setBorder(new EmptyBorder(20, 100, 0, 0));
+    canvas.add(content);
+
+    ButtonGroup buttonGroup = new ButtonGroup();
+    Collection<OptionUiEntry<T>> optionUiEntryList = Lists.newArrayList();
+    for (T option : options) {
+      JPanel vertical = new JPanel(new VerticalLayout(10));
+      JRadioButton radioButton = new JRadioButton();
+      radioButton.setText(option.getOptionText());
+      vertical.add(radioButton);
+
+      JComponent optionComponent = option.getUiComponent();
+      if (optionComponent != null) {
+        JPanel horizontal = new JPanel(new HorizontalLayout(0));
+        horizontal.setBorder(new EmptyBorder(0, 25, 0, 0));
+        horizontal.add(optionComponent);
+        vertical.add(horizontal);
+
+        UiUtil.setEnabledRecursive(optionComponent, false);
+        radioButton.addItemListener(itemEvent -> {
+          boolean isSelected = radioButton.isSelected();
+          UiUtil.setEnabledRecursive(optionComponent, isSelected);
+        });
+      }
+
+      content.add(vertical);
+      buttonGroup.add(radioButton);
+      optionUiEntryList.add(new OptionUiEntry<>(option, radioButton));
+    }
+
+    OptionUiEntry selected = null;
+    String previouslyChosenOption = userSettings.get(getOptionKey(), null);
+    if (previouslyChosenOption != null) {
+      for (OptionUiEntry<T> entry : optionUiEntryList) {
+        if (entry.option.getOptionName().equals(previouslyChosenOption)) {
+          selected = entry;
+          break;
+        }
+      }
+    }
+    if (selected == null) {
+      selected = Iterables.getFirst(optionUiEntryList, null);
+    }
+    if (selected != null) {
+      selected.radioButton.setSelected(true);
+    }
+
+    this.canvas = canvas;
+    this.optionUiEntryList = optionUiEntryList;
+  }
+
+  public BlazeValidationResult validate() {
+    T option = getSelectedOption();
+    if (option == null) {
+      return BlazeValidationResult.failure("No option selected.");
+    }
+    return option.validate();
+  }
+
+  public JComponent getUiComponent() {
+    return canvas;
+  }
+
+  public T getSelectedOption() {
+    for (OptionUiEntry<T> entry : optionUiEntryList) {
+      if (entry.radioButton.isSelected()) {
+        return entry.option;
+      }
+    }
+    return null;
+  }
+
+  public void commit() {
+    T selectedOption = getSelectedOption();
+    if (selectedOption != null) {
+      userSettings.put(getOptionKey(), selectedOption.getOptionName());
+    }
+  }
+
+  /**
+   * Call when the title changes.
+   */
+  protected void setTitle(String newTitle) {
+    titleLabel.setText(newTitle);
+  }
+
+  abstract String getTitle();
+
+  abstract String getOptionKey();
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectProjectViewControl.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectProjectViewControl.java
new file mode 100644
index 0000000..9668287
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectProjectViewControl.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.wizard2.ui;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.BlazeSelectProjectViewOption;
+import com.google.idea.blaze.base.wizard2.BlazeWizardOptionProvider;
+
+import javax.swing.*;
+import java.util.Collection;
+
+
+/**
+ * UI for selecting the project view during the import process.
+ */
+public class BlazeSelectProjectViewControl {
+
+  private BlazeSelectOptionControl<BlazeSelectProjectViewOption> selectOptionControl;
+
+  public BlazeSelectProjectViewControl(BlazeNewProjectBuilder builder) {
+    Collection<BlazeSelectProjectViewOption> options = Lists.newArrayList();
+    for (BlazeWizardOptionProvider optionProvider : BlazeWizardOptionProvider.EP_NAME.getExtensions()) {
+      options.addAll(optionProvider.getSelectProjectViewOptions(builder));
+    }
+
+    this.selectOptionControl =
+      new BlazeSelectOptionControl<BlazeSelectProjectViewOption>(builder, options) {
+        @Override
+        String getTitle() {
+          return BlazeSelectProjectViewControl.getTitle(builder);
+        }
+
+        @Override
+        String getOptionKey() {
+          return "select-project-view.selected-option";
+        }
+      };
+  }
+
+  public JComponent getUiComponent() {
+    return selectOptionControl.getUiComponent();
+  }
+
+  public BlazeValidationResult validate() {
+    return selectOptionControl.validate();
+  }
+
+  public void update(BlazeNewProjectBuilder builder) {
+    selectOptionControl.setTitle(getTitle(builder));
+  }
+
+  public void updateBuilder(BlazeNewProjectBuilder builder) {
+    builder.setProjectViewOption(selectOptionControl.getSelectedOption());
+  }
+
+  public void commit() {
+    selectOptionControl.commit();
+  }
+
+  private static String getTitle(BlazeNewProjectBuilder builder) {
+    String projectViewString = ProjectViewStorageManager.getProjectViewFileName(builder.getBuildSystem());
+    return String.format("Select project view (%s file)", projectViewString);
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java
new file mode 100644
index 0000000..d32deb1
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/BlazeSelectWorkspaceControl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.wizard2.ui;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.BlazeSelectWorkspaceOption;
+import com.google.idea.blaze.base.wizard2.BlazeWizardOptionProvider;
+
+import javax.swing.*;
+import java.util.Collection;
+
+
+/**
+ * UI for selecting a client during the import process.
+ */
+public class BlazeSelectWorkspaceControl {
+  BlazeSelectOptionControl<BlazeSelectWorkspaceOption> selectOptionControl;
+
+  public BlazeSelectWorkspaceControl(BlazeNewProjectBuilder builder) {
+    Collection<BlazeSelectWorkspaceOption> options = Lists.newArrayList();
+    for (BlazeWizardOptionProvider optionProvider : BlazeWizardOptionProvider.EP_NAME.getExtensions()) {
+      options.addAll(optionProvider.getSelectWorkspaceOptions(builder));
+    }
+
+    this.selectOptionControl =
+      new BlazeSelectOptionControl<BlazeSelectWorkspaceOption>(builder, options) {
+        @Override
+        String getTitle() {
+          return "Select workspace";
+        }
+
+        @Override
+        String getOptionKey() {
+          return "select-workspace.selected-option";
+        }
+      };
+  }
+
+  public JComponent getUiComponent() {
+    return selectOptionControl.getUiComponent();
+  }
+
+  public BlazeValidationResult validate() {
+    return selectOptionControl.validate();
+  }
+
+  public void updateBuilder(BlazeNewProjectBuilder builder) {
+    builder.setWorkspaceOption(selectOptionControl.getSelectedOption());
+  }
+
+  public void commit() {
+    selectOptionControl.commit();
+  }
+}
diff --git a/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/SelectBazelBinaryControl.java b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/SelectBazelBinaryControl.java
new file mode 100644
index 0000000..d2acbb6
--- /dev/null
+++ b/blaze-base/src/com/google/idea/blaze/base/wizard2/ui/SelectBazelBinaryControl.java
@@ -0,0 +1,123 @@
+/*
+ * 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.ui;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.settings.ui.BlazeUserSettingsConfigurable;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.ui.FileSelectorWithStoredHistory;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.intellij.ui.components.panels.VerticalLayout;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import javax.swing.border.EmptyBorder;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+
+/**
+ * UI for selecting the build system binary during the import process.
+ */
+public class SelectBazelBinaryControl {
+
+  public final BlazeNewProjectBuilder builder;
+
+  private boolean uiInitialized = false;
+  private JPanel component;
+  private FileSelectorWithStoredHistory bazelBinaryPath;
+
+  public SelectBazelBinaryControl(BlazeNewProjectBuilder builder) {
+    this.builder = builder;
+  }
+
+  public JComponent getUiComponent() {
+    if (!uiInitialized) {
+      initUi();
+      uiInitialized = true;
+    }
+    return component;
+  }
+
+  private void initUi() {
+    bazelBinaryPath = FileSelectorWithStoredHistory.create(
+      BlazeUserSettingsConfigurable.BAZEL_BINARY_PATH_KEY,
+      "Specify the bazel binary path");
+    bazelBinaryPath.setText(getInitialBinaryPath());
+
+    component = new JPanel(new VerticalLayout(4));
+    component.add(new JLabel("Select a bazel binary"));
+    component.add(new JSeparator());
+
+    JPanel content = new JPanel(new VerticalLayout(12));
+    content.setBorder(new EmptyBorder(50, 100, 0, 100));
+    component.add(content);
+
+    content.add(new JLabel("Specify a bazel binary to be used for all bazel projects"));
+    content.add(bazelBinaryPath);
+  }
+
+  public BlazeValidationResult validate() {
+    String binaryPath = getBazelPath();
+    if (Strings.isNullOrEmpty(binaryPath)) {
+      return BlazeValidationResult.failure("Select a bazel binary");
+    }
+    if (!FileAttributeProvider.getInstance().isFile(new File(binaryPath))) {
+      return BlazeValidationResult.failure("Invalid bazel binary: file does not exist");
+    }
+    return BlazeValidationResult.success();
+  }
+
+  public void commit() {
+    if (!Strings.isNullOrEmpty(getBazelPath())) {
+      BlazeUserSettings.getInstance().setBazelBinaryPath(getBazelPath());
+    }
+  }
+
+  private String getBazelPath() {
+    String text = bazelBinaryPath.getText();
+    return text != null ? text.trim() : "";
+  }
+
+  private static String getInitialBinaryPath() {
+    String existingPath = BlazeUserSettings.getInstance().getBazelBinaryPath();
+    if (existingPath != null) {
+      return existingPath;
+    }
+    return guessBinaryPath();
+  }
+
+  /**
+   * Try to guess an initial binary path
+   */
+  @Nullable
+  private static String guessBinaryPath() {
+    ByteArrayOutputStream stdout = new ByteArrayOutputStream();
+    int retVal = ExternalTask.builder(new File("/"), ImmutableList.of("which", "bazel"))
+      .stdout(stdout)
+      .build()
+      .run();
+
+    if (retVal != 0) {
+      return null;
+    }
+    return stdout.toString().trim();
+  }
+
+}
diff --git a/blaze-base/src/icons/BlazeIcons.java b/blaze-base/src/icons/BlazeIcons.java
new file mode 100644
index 0000000..b21c7ef
--- /dev/null
+++ b/blaze-base/src/icons/BlazeIcons.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package icons;
+
+import com.intellij.openapi.util.IconLoader;
+
+import javax.swing.*;
+
+/**
+ * Class to manage icons used by the Blaze plugin.
+ */
+public class BlazeIcons {
+
+  public static final Icon Blaze = load("/blaze-base/resources/icons/blaze.png"); // 16x16
+  public static final Icon BlazeSlow = load("/blaze-base/resources/icons/blaze_slow.png"); // 16x16
+  public static final Icon BlazeDirty = load("/blaze-base/resources/icons/blaze_dirty.png"); // 16x16
+  public static final Icon BlazeClean = load("/blaze-base/resources/icons/blaze_clean.png"); // 16x16
+  public static final Icon BlazeFailed = load("/blaze-base/resources/icons/blaze_failed.png"); // 16x16
+  // This is just the Blaze icon scaled down to the size IJ wants for tool windows.
+  public static final Icon BlazeToolWindow = load("/blaze-base/resources/icons/blazeToolWindow.png"); // 13x13
+
+  public static final Icon BazelLeaf = load("/blaze-base/resources/icons/bazel_leaf.png"); // 16x16
+
+  // Build file support icons
+  public static final Icon BuildFile = load("/blaze-base/resources/icons/build_file.png"); // 16x16
+  public static final Icon BuildRule = load("/blaze-base/resources/icons/build_rule.png"); // 16x16
+
+  private static Icon load(String path) {
+    return IconLoader.getIcon(path, BlazeIcons.class);
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributorTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributorTest.java
new file mode 100644
index 0000000..31ec67c
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ArgumentCompletionContributorTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.openapi.editor.Editor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for code completion of funcall arguments.
+ */
+public class ArgumentCompletionContributorTest extends BuildFileIntegrationTestCase {
+
+  public void testIncompleteFuncall() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "def function(name, deps, srcs):",
+      "  # empty function",
+      "function(d");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 2, "function(n".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isNull();
+
+    assertFileContents(
+      file,
+      "def function(name, deps, srcs):",
+      "  # empty function",
+      "function(deps");
+  }
+
+  public void testExistingKeywordArg() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "def function(name, deps, srcs):",
+      "  # empty function",
+      "function(name = \"lib\")");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 2, "function(".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).hasLength(4);
+    assertThat(completionItems).asList().containsAllOf("name", "deps", "srcs", "function");
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributorTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributorTest.java
new file mode 100644
index 0000000..7b0f616
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionAttributeCompletionContributorTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.AttributeDefinition;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.repackaged.devtools.build.lib.query2.proto.proto2api.Build;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for BuiltInFunctionAttributeCompletionContributor.
+ */
+public class BuiltInFunctionAttributeCompletionContributorTest extends BuildFileIntegrationTestCase {
+
+  private MockBuildLanguageSpecProvider specProvider;
+
+  @Override
+  protected void doSetup() {
+    super.doSetup();
+    specProvider = new MockBuildLanguageSpecProvider();
+    registerApplicationService(BuildLanguageSpecProvider.class, specProvider);
+  }
+
+  public void testSimpleCompletion() {
+    setRuleAndAttributes("sh_binary",
+                         "name", "deps", "srcs", "data");
+
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "sh_binary(");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 0, "sh_binary(".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).asList().containsAllOf("name", "deps", "srcs", "data");
+  }
+
+  public void testSimpleSingleCompletion() {
+    setRuleAndAttributes("sh_binary",
+                         "name", "deps", "srcs", "data");
+
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "sh_binary(",
+      "    n");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 1, "    n".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).isNull();
+    assertFileContents(file,
+                       "sh_binary(",
+                       "    name");
+  }
+
+  public void testNoCompletionInUnknownRule() {
+    setRuleAndAttributes("sh_binary",
+                         "name", "deps", "srcs", "data");
+
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "java_binary(");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 0, "java_binary(".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isEmpty();
+  }
+
+  public void testCompletionInSkylarkExtension() {
+    setRuleAndAttributes("sh_binary",
+                         "name", "deps", "srcs", "data");
+
+    BuildFile file = createBuildFile(
+      "skylark.bzl",
+      "native.sh_binary(");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 0, "native.sh_binary(".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).asList().containsAllOf("name", "deps", "srcs", "data");
+  }
+
+  private void setRuleAndAttributes(String ruleName, String... attributes) {
+    ImmutableMap.Builder<String, AttributeDefinition> map = ImmutableMap.builder();
+    for (String attr : attributes) {
+      map.put(attr, new AttributeDefinition(attr, Build.Attribute.Discriminator.UNKNOWN, false, null, null));
+    }
+    RuleDefinition rule = new RuleDefinition(ruleName, map.build(), null);
+    specProvider.setRules(ImmutableMap.of(ruleName, rule));
+  }
+
+  private static class MockBuildLanguageSpecProvider implements BuildLanguageSpecProvider {
+
+    BuildLanguageSpec languageSpec;
+
+    void setRules(ImmutableMap<String, RuleDefinition> rules) {
+      languageSpec = new BuildLanguageSpec(rules);
+    }
+
+    @Nullable
+    @Override
+    public BuildLanguageSpec getLanguageSpec(Project project) {
+      return languageSpec;
+    }
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributorTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributorTest.java
new file mode 100644
index 0000000..008217b
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/BuiltInFunctionCompletionContributorTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpec;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.BuildLanguageSpecProvider;
+import com.google.idea.blaze.base.lang.buildfile.language.semantics.RuleDefinition;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests BuiltInFunctionCompletionContributor
+ */
+public class BuiltInFunctionCompletionContributorTest extends BuildFileIntegrationTestCase {
+
+  private MockBuildLanguageSpecProvider specProvider;
+
+  @Override
+  protected void doSetup() {
+    super.doSetup();
+    specProvider = new MockBuildLanguageSpecProvider();
+    registerApplicationService(BuildLanguageSpecProvider.class, specProvider);
+  }
+
+  public void testSimpleTopLevelCompletion() {
+    setRules("java_library", "android_binary");
+
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    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");
+
+    assertFileContents(file, "");
+  }
+
+  public void testUniqueTopLevelCompletion() {
+    setRules("java_library", "android_binary");
+
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "ja");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 0, 2);
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isNull();
+
+    assertFileContents(file, "java_library()");
+    assertCaretPosition(editor, 0, "java_library(".length());
+  }
+
+  public void testSkylarkNativeCompletion() {
+    setRules("java_library", "android_binary");
+
+    BuildFile file = createBuildFile(
+      "build_defs.bzl",
+      "def function():",
+      "  native.j");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 1, "  native.j".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isNull();
+
+    assertFileContents(file,
+                       "def function():",
+                       "  native.java_library()");
+    assertCaretPosition(editor, 1, "  native.java_library(".length());
+  }
+
+  public void testNoCompletionInsideRule() {
+    setRules("java_library", "android_binary");
+
+    String[] contents = {
+      "java_library(",
+      "    name = \"lib\"",
+      ""};
+
+    BuildFile file = createBuildFile("BUILD", contents);
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 2, 0);
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isEmpty();
+    assertFileContents(file, contents);
+  }
+
+  private void setRules(String... ruleNames) {
+    ImmutableMap.Builder<String, RuleDefinition> rules = ImmutableMap.builder();
+    for (String name : ruleNames) {
+      rules.put(name, new RuleDefinition(name, ImmutableMap.of(), null));
+    }
+    specProvider.setRules(rules.build());
+  }
+
+  private static class MockBuildLanguageSpecProvider implements BuildLanguageSpecProvider {
+
+    BuildLanguageSpec languageSpec;
+
+    void setRules(ImmutableMap<String, RuleDefinition> rules) {
+      languageSpec = new BuildLanguageSpec(rules);
+    }
+
+    @Nullable
+    @Override
+    public BuildLanguageSpec getLanguageSpec(Project project) {
+      return languageSpec;
+    }
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.java
new file mode 100644
index 0000000..36b8857
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/FilePathCompletionTest.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.lang.buildfile.completion;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests file path code completion in BUILD file labels.
+ */
+public class FilePathCompletionTest extends BuildFileIntegrationTestCase {
+
+  public void testUniqueDirectoryCompleted() {
+    BuildFile file = createBuildFile(
+      "java/BUILD",
+      "'//'");
+
+    Editor editor = openFileInEditor(file);
+    setCaretPosition(editor, 0, "'//".length());
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "'//java'");
+    // check caret remains inside closing quote
+    assertCaretPosition(editor, 0, "'//java".length());
+  }
+
+  public void testUniqueMultiSegmentDirectoryCompleted() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "'//'");
+
+    Editor editor = openFileInEditor(file);
+    setCaretPosition(editor, 0, "'//".length());
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "'//java/com/google'");
+  }
+
+  // expected to be a typical workflow -- complete a segment, get the possibilities, then start typing
+  // next segment and complete again
+  public void testMultiStageCompletion() {
+    createDirectory("foo");
+    createDirectory("bar");
+    createDirectory("other");
+    createDirectory("other/foo");
+    createDirectory("other/bar");
+
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "'//'");
+
+    Editor editor = openFileInEditor(file);
+    setCaretPosition(editor, 0, "'//".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).hasLength(3);
+
+    performTypingAction(editor, 'o');
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "'//other'");
+    assertCaretPosition(editor, 0, "'//other".length());
+
+    performTypingAction(editor, '/');
+    performTypingAction(editor, 'f');
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "'//other/foo'");
+    assertCaretPosition(editor, 0, "'//other/foo".length());
+  }
+
+  public void testCompletionSuggestionString() {
+    createDirectory("foo");
+    createDirectory("bar");
+    createDirectory("other");
+    createDirectory("ostrich/foo");
+    createDirectory("ostrich/fooz");
+
+    VirtualFile file = createAndSetCaret(
+      "BUILD",
+      "'//o<caret>'");
+
+    String[] completionItems = getCompletionItemsAsSuggestionStrings();
+    assertThat(completionItems).asList().containsExactly("other", "ostrich");
+
+    performTypingAction(testFixture.getEditor(), 's');
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "'//ostrich'");
+
+    completionItems = getCompletionItemsAsSuggestionStrings();
+    assertThat(completionItems).asList().containsExactly("/foo", "/fooz");
+
+    performTypingAction(testFixture.getEditor(), '/');
+
+    completionItems = getCompletionItemsAsSuggestionStrings();
+    assertThat(completionItems).asList().containsExactly("foo", "fooz");
+
+    performTypingAction(testFixture.getEditor(), 'f');
+
+    completionItems = getCompletionItemsAsSuggestionStrings();
+    assertThat(completionItems).asList().containsExactly("foo", "fooz");
+  }
+
+  private VirtualFile createAndSetCaret(String filePath, String... fileContents) {
+    VirtualFile file = createFile(filePath, fileContents);
+    testFixture.configureFromExistingVirtualFile(file);
+    return file;
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/LocalSymbolCompletionTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/LocalSymbolCompletionTest.java
new file mode 100644
index 0000000..4d24fab
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/LocalSymbolCompletionTest.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.base.lang.buildfile.completion;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.intellij.psi.PsiFile;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests code completion works with general symbols in scope.
+ */
+public class LocalSymbolCompletionTest extends BuildFileIntegrationTestCase {
+
+  private PsiFile setInput(String... fileContents) {
+    return testFixture.configureByText("BUILD", Joiner.on("\n").join(fileContents));
+  }
+
+  private void assertResult(String... resultingFileContents) {
+    String s = testFixture.getFile().getText();
+    testFixture.checkResult(Joiner.on("\n").join(resultingFileContents));
+  }
+
+  public void testLocalVariable() {
+    setInput(
+      "var = [a, b]",
+      "def function(name, deps, srcs):",
+      "  v<caret>"
+    );
+
+    completeIfUnique();
+
+    assertResult(
+      "var = [a, b]",
+      "def function(name, deps, srcs):",
+      "  var<caret>"
+    );
+  }
+
+  public void testLocalFunction() {
+    setInput(
+      "def fnName():return True",
+      "def function(name, deps, srcs):",
+      "  fnN<caret>"
+    );
+
+    completeIfUnique();
+
+    assertResult(
+      "def fnName():return True",
+      "def function(name, deps, srcs):",
+      "  fnName<caret>"
+    );
+  }
+
+  public void testNoCompletionAfterDot() {
+    setInput(
+      "var = [a, b]",
+      "def function(name, deps, srcs):",
+      "  ext.v<caret>"
+    );
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).isEmpty();
+  }
+
+  public void testFunctionParam() {
+    setInput(
+      "def test(var):",
+      "  v<caret>"
+    );
+
+    completeIfUnique();
+
+    assertResult(
+      "def test(var):",
+      "  var<caret>"
+    );
+  }
+
+  // b/28912523: when symbol is present in multiple assignment statements, should only be
+  // included once in the code-completion dialog
+  public void testSymbolAssignedMultipleTimes() {
+    setInput(
+      "var = 1",
+      "var = 2",
+      "var = 3",
+      "<caret>"
+    );
+
+    completeIfUnique();
+
+    assertResult(
+      "var = 1",
+      "var = 2",
+      "var = 3",
+      "var<caret>"
+    );
+  }
+
+  public void testSymbolDefinedOutsideScope() {
+    setInput(
+      "<caret>",
+      "var = 1"
+    );
+
+    assertThat(getCompletionItemsAsStrings()).isEmpty();
+  }
+
+  public void testSymbolDefinedOutsideScope2() {
+    setInput(
+      "def fn():",
+      "  var = 1",
+      "v<caret>"
+    );
+
+    assertThat(testFixture.completeBasic()).isEmpty();
+  }
+
+  public void testSymbolDefinedOutsideScope3() {
+    setInput(
+      "for var in (1, 2, 3): print var",
+      "v<caret>"
+    );
+
+    assertThat(testFixture.completeBasic()).isEmpty();
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributorTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributorTest.java
new file mode 100644
index 0000000..9aeb6ba
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/ParameterCompletionContributorTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.openapi.editor.Editor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests ParameterCompletionContributor.
+ */
+public class ParameterCompletionContributorTest extends BuildFileIntegrationTestCase {
+
+  public void testArgsCompletion() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "def function(arg1, *");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 0, "def function(arg1, *".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isNull();
+
+    assertFileContents(file, "def function(arg1, *args");
+  }
+
+  public void testKwargsCompletion() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "def function(arg1, **");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 0, "def function(arg1, **".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isNull();
+
+    assertFileContents(file, "def function(arg1, **kwargs");
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java
new file mode 100644
index 0000000..a2e8599
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/RuleTargetCompletionTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.completion;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.openapi.editor.Editor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests code completion of rule target labels.
+ */
+public class RuleTargetCompletionTest extends BuildFileIntegrationTestCase {
+
+  public void testLocalTarget() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(name = 'lib')",
+      "java_library(",
+      "    name = 'test',",
+      "    deps = [':']");
+
+    Editor editor = openFileInEditor(file);
+    setCaretPosition(editor, 3, "    deps = [':".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).hasLength(1);
+    assertThat(completionItems[0].toString()).isEqualTo("':lib'");
+  }
+
+  public void testIgnoreContainingTarget() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(",
+      "    name = 'lib',",
+      "    deps = [':']");
+
+    Editor editor = openFileInEditor(file);
+    setCaretPosition(editor, 2, "    deps = [':".length());
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems).isEmpty();
+  }
+
+  public void testNotCodeCompletionInNameField() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(name = 'lib')",
+      "java_library(",
+      "    name = 'l'",
+      ")");
+
+    Editor editor = openFileInEditor(file);
+    setCaretPosition(editor, 2, "    name = 'l".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).isEmpty();
+  }
+
+  public void testNonLocalTarget() {
+    BuildFile foo = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "java_library(name = 'foo_lib')");
+
+    BuildFile bar = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "java_library(",
+      "    name = 'bar_lib',",
+      "    deps = '//java/com/google/foo:')");
+
+    Editor editor = openFileInEditor(bar);
+    setCaretPosition(editor, 2, "    deps = '//java/com/google/foo:".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).asList().containsExactly("'//java/com/google/foo:foo_lib'");
+  }
+
+  public void testNonLocalRulesNotCompletedWithoutColon() {
+    BuildFile foo = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "java_library(name = 'foo_lib')");
+
+    BuildFile bar = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "java_library(",
+      "    name = 'bar_lib',",
+      "    deps = '//java/com/google/foo')");
+
+    Editor editor = openFileInEditor(bar);
+    setCaretPosition(editor, 2, "    deps = '//java/com/google/foo".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).isEmpty();
+  }
+
+  public void testPackageLocalRulesCompletedWithoutColon() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(name = 'lib')",
+      "java_library(",
+      "    name = 'test',",
+      "    deps = ['']");
+
+    Editor editor = openFileInEditor(file);
+    setCaretPosition(editor, 3, "    deps = ['".length());
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(
+      file,
+      "java_library(name = 'lib')",
+      "java_library(",
+      "    name = 'test',",
+      "    deps = ['lib']");
+  }
+
+  public void testLocalPathIgnoredForNonLocalLabels() {
+    BuildFile rootPackage = createBuildFile(
+      "java/BUILD",
+      "java_library(name = 'root_rule')");
+
+    BuildFile otherPackage = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(",
+      "java_library(name = 'other_rule')",
+      "    name = 'lib',",
+      "    deps = ['//java:']");
+
+    Editor editor = openFileInEditor(otherPackage);
+    setCaretPosition(editor, 3, "    deps = ['//java:".length());
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).asList().contains("'//java:root_rule'");
+    assertThat(completionItems).asList().doesNotContain("'//java/com/google:other_rule'");
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionCompletionTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionCompletionTest.java
new file mode 100644
index 0000000..01bac37
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionCompletionTest.java
@@ -0,0 +1,164 @@
+/*
+ * 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.BuildFileIntegrationTestCase;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests auto-complete of skylark bzl files in 'load' statements.
+ */
+public class SkylarkExtensionCompletionTest extends BuildFileIntegrationTestCase {
+
+  private VirtualFile createAndSetCaret(String filePath, String... fileContents) {
+    VirtualFile file = createFile(filePath, fileContents);
+    testFixture.configureFromExistingVirtualFile(file);
+    return file;
+  }
+
+  public void testSimpleCase() {
+    createFile("skylark.bzl");
+    VirtualFile file = createAndSetCaret(
+      "BUILD",
+      "load(':<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load(':skylark.bzl'");
+  }
+
+  public void testSelfNotInResults() {
+    createFile("BUILD");
+    VirtualFile file = createAndSetCaret(
+      "self.bzl",
+      "load(':<caret>'");
+
+    assertThat(testFixture.completeBasic()).isEmpty();
+  }
+
+  public void testSelfNotInResults2() {
+    createFile("skylark.bzl");
+    createFile("BUILD");
+    VirtualFile file = createAndSetCaret(
+      "self.bzl",
+      "load(':<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load(':skylark.bzl'");
+  }
+
+  public void testNoRulesInResults() {
+    createFile("java/com/google/foo/skylark.bzl");
+    createFile(
+      "java/com/google/foo/BUILD",
+      "java_library(name = 'foo')");
+    VirtualFile file = createAndSetCaret(
+      "java/com/google/bar/BUILD",
+      "load('//java/com/google/foo:<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load('//java/com/google/foo:skylark.bzl'");
+
+    // now check that the rule would have been picked up outside of the 'load' context
+    file = createAndSetCaret(
+      "java/com/google/baz/BUILD",
+      "'//java/com/google/foo:<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "'//java/com/google/foo:foo'");
+  }
+
+  public void testNonSkylarkFilesNotInResults() {
+    createFile("java/com/google/foo/text.txt");
+
+    VirtualFile file = createAndSetCaret(
+      "java/com/google/bar/BUILD",
+      "load('//java/com/google/foo:<caret>'");
+
+    assertThat(testFixture.completeBasic()).isEmpty();
+  }
+
+  public void testLabelStartsWithColon() {
+    createFile("java/com/google/skylark.bzl");
+    VirtualFile file = createAndSetCaret(
+      "java/com/google/BUILD",
+      "load(':<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load(':skylark.bzl'");
+  }
+
+  public void testLabelStartsWithSlashes() {
+    createFile("java/com/google/skylark.bzl");
+    VirtualFile file = createAndSetCaret(
+      "java/com/google/BUILD",
+      "load('//java/com/google:<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load('//java/com/google:skylark.bzl'");
+  }
+
+  public void testLabelStartsWithSlashesWithoutColon() {
+    createFile("java/com/google/skylark.bzl");
+    VirtualFile file = createAndSetCaret(
+      "java/com/google/BUILD",
+      "load('//java/com/google<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load('//java/com/google:skylark.bzl'");
+  }
+
+  public void testDirectoryCompletionInLoadStatement() {
+    createFile("java/com/google/skylark.bzl");
+    VirtualFile file = createAndSetCaret(
+      "java/com/google/BUILD",
+      "load('//<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load('//java/com/google'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load('//java/com/google:skylark.bzl'");
+  }
+
+  public void testMultipleFiles() {
+    createFile("java/com/google/skylark.bzl");
+    createFile("java/com/google/other.bzl");
+    VirtualFile file = createAndSetCaret(
+      "java/com/google/BUILD",
+      "load('//java/com/google:<caret>'");
+
+    String[] strings = getCompletionItemsAsStrings();
+    assertThat(strings).hasLength(2);
+    assertThat(strings).asList().containsExactly("'//java/com/google:other.bzl'", "'//java/com/google:skylark.bzl'");
+  }
+
+  // relative paths in skylark extensions which lie in subdirectories are relative to the parent blaze package directory
+  public void testRelativePathInSubdirectory() {
+    createFile("java/com/google/BUILD");
+    createFile(
+      "java/com/google/nonPackageSubdirectory/skylark.bzl",
+      "def function(): return");
+    VirtualFile file = createAndSetCaret(
+      "java/com/google/nonPackageSubdirectory/other.bzl",
+      "load(':n<caret>'");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load(':nonPackageSubdirectory/skylark.bzl'");
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.java
new file mode 100644
index 0000000..c5870e5
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/completion/SkylarkExtensionSymbolCompletionTest.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.base.lang.buildfile.completion;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests auto-complete of symbols loaded from skylark bzl files.
+ */
+public class SkylarkExtensionSymbolCompletionTest extends BuildFileIntegrationTestCase {
+
+  private VirtualFile createAndSetCaret(String filePath, String... fileContents) {
+    VirtualFile file = createFile(filePath, fileContents);
+    testFixture.configureFromExistingVirtualFile(file);
+    return file;
+  }
+
+  public void testGlobalVariable() {
+    createFile(
+      "skylark.bzl",
+      "VAR = []");
+    VirtualFile file = createAndSetCaret(
+      "BUILD",
+      "load(':skylark.bzl', '<caret>')");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load(':skylark.bzl', 'VAR')");
+  }
+
+  public void testFunctionStatement() {
+    createFile(
+      "skylark.bzl",
+      "def fn(param):stmt");
+    VirtualFile file = createAndSetCaret(
+      "BUILD",
+      "load(':skylark.bzl', '<caret>')");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load(':skylark.bzl', 'fn')");
+  }
+
+  public void testMultipleOptions() {
+    createFile(
+      "skylark.bzl",
+      "def fn(param):stmt",
+      "VAR = []");
+    VirtualFile file = createAndSetCaret(
+      "BUILD",
+      "load(':skylark.bzl', '<caret>')");
+
+    String[] options = getCompletionItemsAsStrings();
+    assertThat(options).asList().containsExactly("'fn'", "'VAR'");
+  }
+
+  public void testRulesNotIncluded() {
+    createFile(
+      "skylark.bzl",
+      "java_library(name = 'lib')",
+      "native.java_library(name = 'foo'");
+
+    VirtualFile file = createAndSetCaret(
+      "BUILD",
+      "load(':skylark.bzl', '<caret>')");
+
+    assertThat(testFixture.completeBasic()).isEmpty();
+  }
+
+  public void testLoadedSymbols() {
+    createFile(
+      "other.bzl",
+      "def function()");
+    createFile(
+      "skylark.bzl",
+      "load(':other.bzl', 'function')");
+    VirtualFile file = createAndSetCaret(
+      "BUILD",
+      "load(':skylark.bzl', '<caret>')");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load(':skylark.bzl', 'function')");
+  }
+
+  public void testNotLoadedSymbolsAreNotIncluded() {
+    createFile(
+      "other.bzl",
+      "def function():stmt",
+      "def other_function():stmt");
+    createFile(
+      "skylark.bzl",
+      "load(':other.bzl', 'function')");
+    VirtualFile file = createAndSetCaret(
+      "BUILD",
+      "load(':skylark.bzl', '<caret>')");
+
+    assertThat(completeIfUnique()).isTrue();
+    assertFileContents(file, "load(':skylark.bzl', 'function')");
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildBraceMatcherTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildBraceMatcherTest.java
new file mode 100644
index 0000000..7f2544c
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildBraceMatcherTest.java
@@ -0,0 +1,140 @@
+/*
+ * 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.editor;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.intellij.psi.PsiFile;
+
+/**
+ * Test brace matching (auto-inserting closing braces when appropriate)
+ */
+public class BuildBraceMatcherTest extends BuildFileIntegrationTestCase {
+
+  private PsiFile setInput(String... fileContents) {
+    return testFixture.configureByText("BUILD", Joiner.on("\n").join(fileContents));
+  }
+
+  public void testClosingParenInserted() {
+    PsiFile file = setInput(
+      "java_library<caret>"
+    );
+
+    performTypingAction(testFixture.getEditor(), '(');
+
+    assertFileContents(
+      file,
+      "java_library()"
+    );
+  }
+
+  public void testClosingBraceInserted() {
+    PsiFile file = setInput(
+      "<caret>"
+    );
+
+    performTypingAction(testFixture.getEditor(), '{');
+
+    assertFileContents(
+      file,
+      "{}"
+    );
+  }
+
+
+  public void testClosingBracketInserted() {
+    PsiFile file = setInput(
+      "<caret>"
+    );
+
+    performTypingAction(testFixture.getEditor(), '[');
+
+    assertFileContents(
+      file,
+      "[]"
+    );
+  }
+
+  public void testNoClosingBracketInsertedIfLaterDanglingRBracket() {
+    PsiFile file = setInput(
+      "java_library(",
+      "    srcs =<caret> 'source.java']",
+      ")"
+    );
+
+    performTypingAction(testFixture.getEditor(), '[');
+
+    assertFileContents(
+      file,
+      "java_library(",
+      "    srcs =[ 'source.java']",
+      ")"
+    );
+  }
+
+  public void testClosingBracketInsertedIfFollowedByWhitespace() {
+    PsiFile file = setInput(
+      "java_library(",
+      "    srcs =<caret> 'source.java'",
+      ")"
+    );
+
+    performTypingAction(testFixture.getEditor(), '[');
+
+    assertFileContents(
+      file,
+      "java_library(",
+      "    srcs =[] 'source.java'",
+      ")"
+    );
+  }
+
+  public void testNoClosingBraceInsertedWhenFollowedByIdentifier() {
+    PsiFile file = setInput(
+      "hello = <caret>test"
+    );
+
+    performTypingAction(testFixture.getEditor(), '(');
+
+    assertFileContents(
+      file,
+      "hello = (test"
+    );
+
+    file = setInput(
+      "hello = <caret>test"
+    );
+
+    performTypingAction(testFixture.getEditor(), '[');
+
+    assertFileContents(
+      file,
+      "hello = [test"
+    );
+
+    file = setInput(
+      "hello = <caret>test"
+    );
+
+    performTypingAction(testFixture.getEditor(), '{');
+
+    assertFileContents(
+      file,
+      "hello = {test"
+    );
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildIndentOnEnterTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildIndentOnEnterTest.java
new file mode 100644
index 0000000..06f5c70
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildIndentOnEnterTest.java
@@ -0,0 +1,485 @@
+/*
+ * 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.editor;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.intellij.openapi.actionSystem.IdeActions;
+
+/**
+ * Tests that indents are inserted correctly when enter is pressed.
+ */
+public class BuildIndentOnEnterTest extends BuildFileIntegrationTestCase {
+
+  private void setInput(String... fileContents) {
+    testFixture.configureByText("BUILD", Joiner.on("\n").join(fileContents));
+  }
+
+  private void pressEnterAndAssertResult(String... resultingFileContents) {
+    pressButton(IdeActions.ACTION_EDITOR_ENTER);
+    String s = testFixture.getFile().getText();
+    testFixture.checkResult(Joiner.on("\n").join(resultingFileContents));
+  }
+
+  public void testSimpleIndent() {
+    setInput(
+      "a=1<caret>");
+    pressEnterAndAssertResult(
+      "a=1",
+      "<caret>");
+  }
+
+  public void testAlignInListMiddle() {
+    setInput(
+      "target = [a,<caret>",
+      "          c]");
+    pressEnterAndAssertResult(
+      "target = [a,",
+      "          <caret>",
+      "          c]");
+  }
+
+  public void testNoAlignAfterList() {
+    setInput(
+      "target = [",
+      "    arg",
+      "]<caret>");
+    pressEnterAndAssertResult(
+      "target = [",
+      "    arg",
+      "]",
+      "<caret>");
+  }
+
+  public void testAlignInDict() {
+    setInput(
+      "some_call({'aaa': 'v1',<caret>})");
+    pressEnterAndAssertResult(
+      "some_call({'aaa': 'v1',",
+      "           <caret>})");
+  }
+
+  public void testAlignInDictInParams() {  // PY-1947
+    setInput(
+      "foobar({<caret>})");
+    pressEnterAndAssertResult(
+      "foobar({",
+      "    <caret>",
+      "})");
+  }
+
+  public void testAlignInEmptyList() {
+    setInput(
+      "target = [<caret>]");
+    pressEnterAndAssertResult(
+      "target = [",
+      "    <caret>",
+      "]");
+  }
+
+  public void testAlignInEmptyParens() {
+    setInput(
+      "foo(<caret>)");
+    pressEnterAndAssertResult(
+      "foo(",
+      "    <caret>",
+      ")");
+  }
+
+  public void testAlignInEmptyDict() {
+    setInput(
+      "{<caret>}");
+    pressEnterAndAssertResult(
+      "{",
+      "    <caret>",
+      "}");
+  }
+
+  public void testAlignInEmptyTuple() {
+    setInput(
+      "(<caret>)");
+    pressEnterAndAssertResult(
+      "(",
+      "    <caret>",
+      ")");
+  }
+
+  public void testEnterInNonEmptyArgList() {
+    setInput(
+      "func(<caret>params=1)");
+    pressEnterAndAssertResult(
+      "func(",
+      "    <caret>params=1)");
+  }
+
+  public void testEmptyFuncallStart() {
+    setInput(
+      "func(<caret>",
+      ")");
+    pressEnterAndAssertResult(
+      "func(",
+      "    <caret>",
+      ")");
+  }
+
+  public void testEmptyFuncallAfterNewlineNoIndent() {
+    setInput(
+      "func(",
+      "<caret>)");
+    pressEnterAndAssertResult(
+      "func(",
+      "",
+      "<caret>)");
+  }
+
+  public void testEmptyFuncallAfterNewlineWithIndent() {
+    setInput(
+      "func(",
+      "    <caret>",
+      ")");
+    pressEnterAndAssertResult(
+      "func(",
+      "    ",
+      "    <caret>",
+      ")");
+  }
+
+  public void testFuncallAfterFirstArg() {
+    setInput(
+      "func(",
+      "    arg1,<caret>",
+      ")");
+    pressEnterAndAssertResult(
+      "func(",
+      "    arg1,",
+      "    <caret>",
+      ")");
+  }
+
+  public void testFuncallFirstArgOnSameLine() {
+    setInput(
+      "func(arg1, arg2,<caret>");
+    pressEnterAndAssertResult(
+      "func(arg1, arg2,",
+      "     <caret>");
+  }
+
+  public void testFuncallFirstArgOnSameLineWithClosingBrace() {
+    setInput(
+      "func(arg1, arg2,<caret>)");
+    pressEnterAndAssertResult(
+      "func(arg1, arg2,",
+      "     <caret>)");
+  }
+
+  public void testNonEmptyDict() {
+    setInput(
+      "{key1 : value1,<caret>}");
+    pressEnterAndAssertResult(
+      "{key1 : value1,",
+      " <caret>}");
+  }
+
+  public void testNonEmptyDictFirstArgIndented() {
+    setInput(
+      "{",
+      "    key1 : value1,<caret>" +
+      "}");
+    pressEnterAndAssertResult(
+      "{",
+      "    key1 : value1,",
+      "    <caret>" +
+      "}");
+  }
+
+  public void testEmptyDictAlreadyIndented() {
+    setInput(
+      "{",
+      "    <caret>" +
+      "}");
+    pressEnterAndAssertResult(
+      "{",
+      "    ",
+      "    <caret>" +
+      "}");
+  }
+
+  public void testEmptyParamIndent() {
+    setInput(
+      "def fn(<caret>)");
+    pressEnterAndAssertResult(
+      "def fn(",
+      "    <caret>",
+      ")");
+  }
+
+  public void testNonEmptyParamIndent() {
+    setInput(
+      "def fn(param1,<caret>)");
+    pressEnterAndAssertResult(
+      "def fn(param1,",
+      "       <caret>)");
+  }
+
+  public void testFunctionDefAfterColon() {
+    setInput(
+      "def fn():<caret>");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  <caret>");
+  }
+
+  // def fn():stmt* (THIS IS CURRENTLY BROKEN -- shouldn't indent but does)
+  public void testFunctionDefSingleStatement() {
+    setInput(
+      "def fn():stmt<caret>");
+    pressEnterAndAssertResult(
+      "def fn():stmt",
+      "<caret>");
+  }
+
+  public void testFunctionDefAfterFirstSuiteStatement() {
+    setInput(
+      "def fn():",
+      "  stmt1<caret>");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  stmt1",
+      "  <caret>");
+  }
+
+  public void testNoIndentAfterSuiteDedentOnEmptyLine() {
+    setInput(
+      "def fn():",
+      "  stmt1",
+      "  stmt2",
+      "<caret>");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  stmt1",
+      "  stmt2",
+      "",
+      "<caret>");
+  }
+
+  public void testIndentAfterIf() {
+    setInput(
+      "if condition:<caret>"
+    );
+    pressEnterAndAssertResult(
+      "if condition:",
+      "  <caret>"
+    );
+  }
+
+  public void testNoIndentAfterIfPlusStatement() {
+    setInput(
+      "if condition:stmt<caret>"
+    );
+    pressEnterAndAssertResult(
+      "if condition:stmt",
+      "<caret>"
+    );
+  }
+
+  public void testIndentAfterElseIf() {
+    setInput(
+      "if condition:",
+      "  stmt",
+      "elif:<caret>"
+    );
+    pressEnterAndAssertResult(
+      "if condition:",
+      "  stmt",
+      "elif:",
+      "  <caret>"
+    );
+  }
+
+  public void testNoIndentAfterElseIfPlusStatement() {
+    setInput(
+      "if condition:",
+      "  stmt",
+      "elif:stmt<caret>"
+    );
+    pressEnterAndAssertResult(
+      "if condition:",
+      "  stmt",
+      "elif:stmt",
+      "<caret>"
+    );
+  }
+
+  public void testIndentAfterElse() {
+    setInput(
+      "if condition:",
+      "  stmt",
+      "else:<caret>"
+    );
+    pressEnterAndAssertResult(
+      "if condition:",
+      "  stmt",
+      "else:",
+      "  <caret>"
+    );
+  }
+
+  public void testNoIndentAfterElsePlusStatement() {
+    setInput(
+      "if condition:",
+      "  stmt",
+      "else:stmt<caret>"
+    );
+    pressEnterAndAssertResult(
+      "if condition:",
+      "  stmt",
+      "else:stmt",
+      "<caret>"
+    );
+  }
+
+  public void testIndentAfterForColon() {
+    setInput(
+      "for x in list:<caret>"
+    );
+    pressEnterAndAssertResult(
+      "for x in list:",
+      "  <caret>"
+    );
+  }
+
+  public void testNoIndentAfterForPlusStatement() {
+    setInput(
+      "for x in list:do_action<caret>"
+    );
+    pressEnterAndAssertResult(
+      "for x in list:do_action",
+      "<caret>"
+    );
+  }
+
+  public void testCommonRuleCase1() {
+    setInput(
+      "java_library(",
+      "    name = 'lib'",
+      "    srcs = [<caret>]");
+    pressEnterAndAssertResult(
+      "java_library(",
+      "    name = 'lib'",
+      "    srcs = [",
+      "        <caret>",
+      "    ]");
+  }
+
+  public void testCommonRuleCase2() {
+    setInput(
+      "java_library(",
+      "    name = 'lib'",
+      "    srcs = [",
+      "        'source',<caret>",
+      "    ]");
+    pressEnterAndAssertResult(
+      "java_library(",
+      "    name = 'lib'",
+      "    srcs = [",
+      "        'source',",
+      "        <caret>",
+      "    ]");
+  }
+
+  public void testCommonRuleCase3() {
+    setInput(
+      "java_library(",
+      "    name = 'lib'",
+      "    srcs = ['first',<caret>]");
+    pressEnterAndAssertResult(
+      "java_library(",
+      "    name = 'lib'",
+      "    srcs = ['first',",
+      "            <caret>]");
+  }
+
+  public void testDedentAfterReturn() {
+    setInput(
+      "def fn():",
+      "  return None<caret>");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  return None",
+      "<caret>");
+  }
+
+  public void testDedentAfterEmptyReturn() {
+    setInput(
+      "def fn():",
+      "  return<caret>");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  return",
+      "<caret>");
+  }
+
+  public void testDedentAfterReturnWithTrailingWhitespace() {
+    setInput(
+      "def fn():",
+      "  return<caret>   ");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  return",
+      "<caret>");
+  }
+
+  public void testDedentAfterComplexReturn() {
+    setInput(
+      "def fn():",
+      "  return a == b<caret>");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  return a == b",
+      "<caret>");
+  }
+
+  public void testDedentAfterPass() {
+    setInput(
+      "def fn():",
+      "  pass<caret>");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  pass",
+      "<caret>");
+  }
+
+  public void testDedentAfterPassInLoop() {
+    setInput(
+      "def fn():",
+      "  for a in (1,2,3):",
+      "    pass<caret>");
+    pressEnterAndAssertResult(
+      "def fn():",
+      "  for a in (1,2,3):",
+      "    pass",
+      "  <caret>");
+  }
+
+  // regression test for b/29564041
+  public void testNoExceptionPressingEnterAtStartOfFile() {
+    setInput("#<caret>");
+    pressEnterAndAssertResult(
+      "#",
+      "<caret>");
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandlerTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandlerTest.java
new file mode 100644
index 0000000..77d45ee
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/BuildQuoteHandlerTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.editor;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+
+/**
+ * Tests for BuildQuoteHandler.
+ */
+public class BuildQuoteHandlerTest extends BuildFileIntegrationTestCase {
+
+  public void testClosingQuoteInserted() {
+    BuildFile file = createBuildFile("BUILD", "");
+
+    performTypingAction(file, '"');
+    assertFileContents(file, "\"\"");
+  }
+
+  public void testClosingSingleQuoteInserted() {
+    BuildFile file = createBuildFile("BUILD", "");
+
+    performTypingAction(file, '\'');
+    assertFileContents(file, "''");
+  }
+
+  public void testClosingTripleQuoteInserted() {
+    BuildFile file = createBuildFile("BUILD", "");
+
+    performTypingAction(file, '"');
+    performTypingAction(file, '"');
+    performTypingAction(file, '"');
+    assertFileContents(file, "\"\"\"\"\"\"");
+  }
+
+  public void testClosingTripleSingleQuoteInserted() {
+    BuildFile file = createBuildFile("BUILD", "");
+
+    performTypingAction(file, '\'');
+    performTypingAction(file, '\'');
+    performTypingAction(file, '\'');
+    assertFileContents(file, "''''''");
+  }
+
+  public void testOnlyCaretMovedWhenCompletingExistingClosingQuotes() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "'text<caret>'",
+      "laterContents");
+
+    testFixture.configureFromExistingVirtualFile(file.getVirtualFile());
+
+    performTypingAction(file, '\'');
+
+    testFixture.checkResult(Joiner.on("\n").join(
+      "'text'<caret>",
+      "laterContents"
+    ));
+  }
+
+  public void testOnlyCaretMovedWhenCompletingExistingClosingTripleQuotes() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "'''text<caret>'''",
+      "laterContents");
+
+    testFixture.configureFromExistingVirtualFile(file.getVirtualFile());
+
+    performTypingAction(file, '\'');
+
+    testFixture.checkResult(Joiner.on("\n").join(
+      "'''text'<caret>''",
+      "laterContents"
+    ));
+
+    performTypingAction(file, '\'');
+
+    testFixture.checkResult(Joiner.on("\n").join(
+      "'''text''<caret>'",
+      "laterContents"
+    ));
+
+    performTypingAction(file, '\'');
+
+    testFixture.checkResult(Joiner.on("\n").join(
+      "'''text'''<caret>",
+      "laterContents"
+    ));
+  }
+
+  public void testAdditionalTripleQuotesNotInsertedWhenClosingQuotes() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "'''text''<caret>",
+      "laterContents");
+
+    testFixture.configureFromExistingVirtualFile(file.getVirtualFile());
+
+    performTypingAction(file, '\'');
+
+    testFixture.checkResult(Joiner.on("\n").join(
+      "'''text'''<caret>",
+      "laterContents"
+    ));
+  }
+
+  public void testAdditionalQuoteNotInsertedWhenClosingQuotes() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "'text<caret>",
+      "laterContents");
+
+    testFixture.configureFromExistingVirtualFile(file.getVirtualFile());
+
+    performTypingAction(file, '\'');
+
+    testFixture.checkResult(Joiner.on("\n").join(
+      "'text'<caret>",
+      "laterContents"
+    ));
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/EnterInLineCommentTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/EnterInLineCommentTest.java
new file mode 100644
index 0000000..a48dbf1
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/editor/EnterInLineCommentTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.editor;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.openapi.editor.Editor;
+
+/**
+ * Test that comments are continued when creating a newline mid comment.
+ */
+public class EnterInLineCommentTest extends BuildFileIntegrationTestCase {
+
+  public void testInternalNewlineCommented() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "# first line comment",
+      "# second line comment");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 1, "# second ".length());
+    performTypingAction(editor, '\n');
+    assertFileContents(
+      file,
+      "# first line comment",
+      "# second ",
+      "# line comment");
+    assertCaretPosition(editor, 2, 2);
+  }
+
+  public void testNewlineAtEndOfComment() {
+    BuildFile file = createBuildFile(
+      "BUILD",
+      "# first line comment",
+      "# second line comment");
+
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    setCaretPosition(editor, 1, "# second line comment".length());
+    performTypingAction(editor, '\n');
+    assertFileContents(
+      file,
+      "# first line comment",
+      "# second line comment",
+      "");
+    assertCaretPosition(editor, 2, 0);
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/BlazePackageFindUsagesTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/BlazePackageFindUsagesTest.java
new file mode 100644
index 0000000..b212f9b
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/BlazePackageFindUsagesTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that all references to a blaze package (including in the package components of labels)
+ * are found by the 'Find Usages' action.
+ */
+public class BlazePackageFindUsagesTest extends BuildFileIntegrationTestCase {
+
+  public void testDirectReferenceFound() {
+    BuildFile foo = createBuildFile(
+      "java/com/google/foo/BUILD");
+
+    BuildFile bar = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "package_group(name = \"grp\", packages = [\"//java/com/google/foo\"])");
+
+    PsiReference[] references = FindUsages.findAllReferences(foo);
+    assertThat(references).hasLength(1);
+
+    PsiElement ref = references[0].getElement();
+    assertThat(ref).isInstanceOf(StringLiteral.class);
+    assertThat(ref.getContainingFile()).isEqualTo(bar);
+  }
+
+  public void testLabelFragmentReferenceFound() {
+    BuildFile foo = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "java_library(name = \"lib\")");
+
+    BuildFile bar = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "java_library(name = \"lib2\", exports = [\"//java/com/google/foo:lib\"])");
+
+    PsiReference[] references = FindUsages.findAllReferences(foo);
+    assertThat(references).hasLength(1);
+
+    PsiElement ref = references[0].getElement();
+    assertThat(ref).isInstanceOf(StringLiteral.class);
+    assertThat(ref.getContainingFile()).isEqualTo(bar);
+  }
+
+  /**
+   * If these don't resolve, directory rename refactoring won't update all labels correctly
+   */
+  public void testInternalReferencesResolve() {
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(name = \"lib\")",
+      "java_library(name = \"other\", deps = [\"//java/com/google:lib\"])");
+
+    PsiReference[] references = FindUsages.findAllReferences(buildFile);
+    assertThat(references).hasLength(1);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java
new file mode 100644
index 0000000..a83b080
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/ExternalFileUsagesTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReference;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that references to external files (e.g. Java classes, text files) are found by the 'Find Usages' action
+ */
+public class ExternalFileUsagesTest extends BuildFileIntegrationTestCase {
+
+  public void testJavaClassUsagesFound() {
+    PsiFile javaFile = createPsiFile(
+      "com/google/foo/JavaClass.java",
+      "package com.google.foo;",
+      "public class JavaClass {}");
+
+    BuildFile buildFile = createBuildFile(
+      "com/google/foo/BUILD",
+      "java_library(name = \"lib\", srcs = [\"JavaClass.java\"])");
+
+    PsiReference[] references = FindUsages.findAllReferences(javaFile);
+    assertThat(references).hasLength(1);
+
+    Argument.Keyword arg = buildFile.findChildByClass(FuncallExpression.class)
+      .getKeywordArgument("srcs");
+
+    PsiElement ref = references[0].getElement();
+    assertThat(ref).isInstanceOf(StringLiteral.class);
+    assertThat(PsiUtils.getParentOfType(ref, Argument.Keyword.class))
+      .isEqualTo(arg);
+  }
+
+  public void testTextFileUsagesFound() {
+    PsiFile textFile = createPsiFile("com/google/foo/data.txt");
+
+    BuildFile buildFile = createBuildFile(
+      "com/google/foo/BUILD",
+      "filegroup(name = \"lib\", srcs = [\"data.txt\"])",
+      "filegroup(name = \"lib2\", srcs = [\"//com/google/foo:data.txt\"])");
+
+    PsiReference[] references = FindUsages.findAllReferences(textFile);
+    assertThat(references).hasLength(2);
+  }
+
+  public void testInvalidReferenceDoesntResolve() {
+    BuildFile packageFoo = createBuildFile("com/google/foo/BUILD");
+    PsiFile textFileInFoo = createPsiFile("com/google/foo/data.txt");
+
+    BuildFile packageBar = createBuildFile(
+      "com/google/bar/BUILD",
+      "filegroup(name = \"lib\", srcs = [\":data.txt\"])");
+
+    PsiReference[] references = FindUsages.findAllReferences(textFileInFoo);
+    assertThat(references).isEmpty();
+  }
+
+  public void testSkylarkExtensionUsagesFound() {
+    BuildFile ext = createBuildFile(
+      "com/google/foo/ext.bzl",
+      "def fn(): return");
+    createBuildFile(
+      "com/google/foo/BUILD",
+      "load(':ext.bzl', 'fn')",
+      "load('ext.bzl', 'fn')",
+      "load('//com/google/foo:ext.bzl', 'fn')"
+    );
+
+    PsiReference[] references = FindUsages.findAllReferences(ext);
+    assertThat(references).hasLength(3);
+  }
+
+  public void testSkylarkExtensionInSubDirectoryUsagesFound() {
+    BuildFile ext = createBuildFile(
+      "com/google/foo/subdir/ext.bzl",
+      "def fn(): return");
+    createBuildFile(
+      "com/google/foo/BUILD",
+      "load(':subdir/ext.bzl', 'fn')",
+      "load('subdir/ext.bzl', 'fn')",
+      "load('//com/google/foo:subdir/ext.bzl', 'fn')"
+    );
+
+    PsiReference[] references = FindUsages.findAllReferences(ext);
+    assertThat(references).hasLength(3);
+  }
+
+  public void testSkylarkExtensionInSubDirectoryOfDifferentPackage() {
+    BuildFile otherPkg = createBuildFile(
+      "com/google/foo/BUILD");
+    BuildFile ext = createBuildFile(
+      "com/google/foo/subdir/ext.bzl",
+      "def fn(): return");
+
+    createBuildFile(
+      "com/google/bar/BUILD",
+      "load('//com/google/foo:subdir/ext.bzl', 'fn')"
+    );
+
+    PsiReference[] references = FindUsages.findAllReferences(ext);
+    assertThat(references).hasLength(1);
+  }
+
+  public void testSkylarkExtensionReferencedFromSubpackage() {
+    BuildFile pkg = createBuildFile(
+      "com/google/foo/BUILD");
+    BuildFile ext1 = createBuildFile(
+      "com/google/foo/subdir/testing.bzl",
+      "def fn(): return");
+    BuildFile ext2 = createBuildFile(
+      "com/google/foo/subdir/other.bzl",
+      "load(':subdir/testing.bzl', 'fn')");
+
+    PsiReference[] references = FindUsages.findAllReferences(ext1);
+    assertThat(references).hasLength(1);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindParameterUsagesTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindParameterUsagesTest.java
new file mode 100644
index 0000000..a9a3ae8
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindParameterUsagesTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.ParameterList;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.intellij.psi.PsiReference;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that usages of function parameters (i.e. by named args in funcall expressions) are found
+ */
+public class FindParameterUsagesTest extends BuildFileIntegrationTestCase {
+
+  public void testLocalReferences() {
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/build_defs.bzl",
+      "def function(arg1, arg2)",
+      "function(arg1 = 1, arg2 = \"name\")");
+
+    FunctionStatement fn = buildFile.findChildByClass(FunctionStatement.class);
+    ParameterList params = fn.getParameterList();
+
+    PsiReference[] references = FindUsages.findAllReferences(params.findParameterByName("arg1"));
+    assertThat(references).hasLength(1);
+
+    references = FindUsages.findAllReferences(params.findParameterByName("arg2"));
+    assertThat(references).hasLength(1);
+  }
+
+  public void testNonLocalReferences() {
+    BuildFile foo = createBuildFile(
+      "java/com/google/build_defs.bzl",
+      "def function(arg1, arg2)");
+
+    BuildFile bar = createBuildFile(
+      "java/com/google/other/BUILD",
+      "load(\"//java/com/google:build_defs.bzl\", \"function\")",
+      "function(arg1 = 1, arg2 = \"name\", extra = x)");
+
+    FunctionStatement fn = foo.findChildByClass(FunctionStatement.class);
+    ParameterList params = fn.getParameterList();
+
+    PsiReference[] references = FindUsages.findAllReferences(params.findParameterByName("arg1"));
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement().getContainingFile()).isEqualTo(bar);
+
+    references = FindUsages.findAllReferences(params.findParameterByName("arg2"));
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement().getContainingFile()).isEqualTo(bar);
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindRuleUsagesTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindRuleUsagesTest.java
new file mode 100644
index 0000000..3a24bfd
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FindRuleUsagesTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that usages of build rules are found
+ */
+public class FindRuleUsagesTest extends BuildFileIntegrationTestCase {
+
+  public void testLocalReferences() {
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(name = \"target\")",
+      "top_level_ref = \":target\"",
+      "java_library(name = \"other\", deps = [\":target\"]");
+
+    FuncallExpression target = buildFile.findChildByClass(FuncallExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(2);
+
+    PsiElement firstRef = references[0].getElement();
+    assertThat(firstRef).isInstanceOf(StringLiteral.class);
+    assertThat(firstRef.getParent()).isInstanceOf(AssignmentStatement.class);
+
+    PsiElement secondRef = references[1].getElement();
+    assertThat(secondRef).isInstanceOf(StringLiteral.class);
+    assertThat(secondRef.getParent()).isInstanceOf(ListLiteral.class);
+  }
+
+  // test full package references, made locally
+  public void testLocalFullReference() {
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(name = \"target\")",
+      "java_library(name = \"other\", deps = [\"//java/com/google:target\"]");
+
+    FuncallExpression target = buildFile.findChildByClass(FuncallExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(1);
+
+    PsiElement ref = references[0].getElement();
+    assertThat(ref).isInstanceOf(StringLiteral.class);
+    assertThat(ref.getParent()).isInstanceOf(ListLiteral.class);
+  }
+
+  public void testNonLocalReferences() {
+    BuildFile targetFile = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "java_library(name = \"target\")");
+
+    BuildFile refFile = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "java_library(name = \"ref\", exports = [\"//java/com/google/foo:target\"])");
+
+    FuncallExpression target = targetFile.findChildByClass(FuncallExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(1);
+
+    PsiElement ref = references[0].getElement();
+    assertThat(ref).isInstanceOf(StringLiteral.class);
+    assertThat(ref.getContainingFile()).isEqualTo(refFile);
+  }
+
+  public void testFindUsagesWorksFromNameString() {
+    BuildFile targetFile = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "java_library(name = \"tar<caret>get\")");
+
+    BuildFile refFile = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "java_library(name = \"ref\", exports = [\"//java/com/google/foo:target\"])");
+
+    testFixture.configureFromExistingVirtualFile(targetFile.getVirtualFile());
+
+    PsiElement targetElement = GotoDeclarationAction.findElementToShowUsagesOf(
+      testFixture.getEditor(),
+      testFixture.getEditor().getCaretModel().getOffset());
+
+    PsiReference[] references = FindUsages.findAllReferences(targetElement);
+    assertThat(references).hasLength(1);
+
+    PsiElement ref = references[0].getElement();
+    assertThat(ref).isInstanceOf(StringLiteral.class);
+    assertThat(ref.getContainingFile()).isEqualTo(refFile);
+  }
+
+  public void testInvalidReferenceDoesntResolve() {
+    // reference ":target" from another build file (missing package path in label)
+    BuildFile targetFile = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "java_library(name = \"target\")");
+
+    BuildFile refFile = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "java_library(name = \"ref\", exports = [\":target\"])");
+
+    FuncallExpression target = targetFile.findChildByClass(FuncallExpression.class);
+    assertThat(target).isNotNull();
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(0);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FunctionStatementUsagesTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FunctionStatementUsagesTest.java
new file mode 100644
index 0000000..b2840c3
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/FunctionStatementUsagesTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that usages of function declarations are found
+ */
+public class FunctionStatementUsagesTest extends BuildFileIntegrationTestCase {
+
+  public void testLocalReferences() {
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/build_defs.bzl",
+      "def function(name, srcs, deps):",
+      "    # function body",
+      "function(name = \"foo\")");
+
+    FunctionStatement funcDef = buildFile.findChildByClass(FunctionStatement.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(funcDef);
+    assertThat(references).hasLength(1);
+
+    PsiElement ref = references[0].getElement();
+    assertThat(ref).isInstanceOf(FuncallExpression.class);
+  }
+
+  public void testLoadedFunctionReferences() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/build_defs.bzl",
+      "def function(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google:build_defs.bzl\",",
+      "\"function\"",
+      ")");
+
+    FunctionStatement funcDef = extFile.findChildByClass(FunctionStatement.class);
+    LoadStatement load = buildFile.firstChildOfClass(LoadStatement.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(funcDef);
+    assertThat(references).hasLength(1);
+
+    PsiElement ref = references[0].getElement();
+    assertThat(ref).isInstanceOf(StringLiteral.class);
+    assertThat(ref.getParent()).isEqualTo(load);
+  }
+
+  public void testFuncallReference() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/tools/build_defs.bzl",
+      "def function(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google/tools:build_defs.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", deps = []");
+
+    FunctionStatement function = extFile.firstChildOfClass(FunctionStatement.class);
+    FuncallExpression funcall = buildFile.firstChildOfClass(FuncallExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(function);
+    assertThat(references).hasLength(2);
+
+    assertThat(references[1].getElement()).isEqualTo(funcall);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/GlobFindUsagesTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/GlobFindUsagesTest.java
new file mode 100644
index 0000000..57ddf67
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/GlobFindUsagesTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.GlobExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.google.idea.blaze.base.lang.projectview.language.ProjectViewLanguage;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiManager;
+import com.intellij.psi.PsiReference;
+import com.intellij.psi.impl.PsiManagerEx;
+import com.intellij.psi.impl.file.impl.FileManager;
+import com.intellij.testFramework.LightVirtualFile;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that file references in globs are included in the 'find usages' results.
+ */
+public class GlobFindUsagesTest extends BuildFileIntegrationTestCase {
+
+  public void testSimpleGlobReferencingSingleFile() {
+    PsiFile ref = createPsiFile("java/com/google/Test.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'])");
+
+    PsiReference[] references = FindUsages.findAllReferences(ref);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isInstanceOf(GlobExpression.class);
+  }
+
+  public void testSimpleGlobReferencingSingleFile2() {
+    PsiFile ref = createPsiFile("java/com/google/Test.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(ref);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isEqualTo(glob);
+  }
+
+  public void testSimpleGlobReferencingSingleFile3() {
+    PsiFile ref = createPsiFile("java/com/google/Test.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['T*t.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(ref);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isEqualTo(glob);
+  }
+
+  public void testGlobReferencingMultipleFiles() {
+    PsiFile ref1 = createPsiFile("java/com/google/Test.java");
+    PsiFile ref2 = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(ref1);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isEqualTo(glob);
+
+    references = FindUsages.findAllReferences(ref2);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isEqualTo(glob);
+  }
+
+  public void testFindsSubDirectories() {
+    PsiFile ref1 = createPsiFile("java/com/google/test/Test.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(ref1);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isEqualTo(glob);
+  }
+
+  public void testGlobWithExcludes() {
+    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
+    PsiFile foo = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(" +
+      "  ['**/*.java']," +
+      "  exclude = ['tests/*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(foo);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isEqualTo(glob);
+
+    assertThat(FindUsages.findAllReferences(test)).isEmpty();
+  }
+
+  public void testIncludeDirectories() {
+    PsiDirectory dir = createPsiDirectory("java/com/google/tests");
+    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
+    PsiFile foo = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(" +
+      "  ['**/*']," +
+      "  exclude = ['BUILD']," +
+      "  exclude_directories = 0)");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(dir);
+    assertThat(references).hasLength(1);
+    assertThat(references[0].getElement()).isEqualTo(glob);
+  }
+
+  public void testExcludeDirectories() {
+    PsiDirectory dir = createPsiDirectory("java/com/google/tests");
+    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
+    PsiFile foo = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(" +
+      "  ['**/*']," +
+      "  exclude = ['BUILD'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(dir);
+    assertThat(references).isEmpty();
+  }
+
+  public void testFilesInSubpackagesExcluded() {
+    BuildFile pkg = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'])");
+    BuildFile subPkg = createBuildFile("java/com/google/other/BUILD");
+    createFile("java/com/google/other/Other.java");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(pkg, GlobExpression.class);
+
+    PsiReference[] references = FindUsages.findAllReferences(subPkg);
+    assertThat(references).isEmpty();
+  }
+
+  // regression test for b/29267289
+  public void testInMemoryFileHandledGracefully() {
+    BuildFile pkg = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'])");
+
+    LightVirtualFile inMemoryFile = new LightVirtualFile("mockProjectViewFile", ProjectViewLanguage.INSTANCE, "");
+
+    FileManager fileManager = ((PsiManagerEx) PsiManager.getInstance(getProject())).getFileManager();
+    fileManager.setViewProvider(inMemoryFile, fileManager.createFileViewProvider(inMemoryFile, true));
+
+    PsiFile psiFile = fileManager.findFile(inMemoryFile);
+
+    PsiReference[] references = FindUsages.findAllReferences(psiFile);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java
new file mode 100644
index 0000000..3ff6409
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/findusages/LocalVariableUsagesTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.findusages;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.references.LocalReference;
+import com.google.idea.blaze.base.lang.buildfile.references.TargetReference;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiReference;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that references to local variables are found by the 'Find Usages' action
+ * TODO: Support comprehension suffix, and add test for it
+ */
+public class LocalVariableUsagesTest extends BuildFileIntegrationTestCase {
+
+  public void testLocalReferences() {
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "localVar = 5",
+      "funcall(localVar)",
+      "def function(name):",
+      "    tempVar = localVar");
+
+    TargetExpression target = buildFile
+      .findChildByClass(AssignmentStatement.class)
+      .getLeftHandSideExpression();
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(2);
+
+    FuncallExpression funcall = buildFile.findChildByClass(FuncallExpression.class);
+    assertThat(funcall).isNotNull();
+
+    PsiElement firstRef = references[0].getElement();
+    assertThat(PsiUtils.getParentOfType(firstRef, FuncallExpression.class))
+      .isEqualTo(funcall);
+
+    FunctionStatement function = buildFile.findChildByClass(FunctionStatement.class);
+    assertThat(function).isNotNull();
+
+    PsiElement secondRef = references[1].getElement();
+    assertThat(secondRef.getParent()).isInstanceOf(AssignmentStatement.class);
+    assertThat(PsiUtils.getParentOfType(secondRef, FunctionStatement.class))
+      .isEqualTo(function);
+  }
+
+  // the case where a symbol is the target of multiple assignment statements
+  public void testMultipleAssignments() {
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "var = 5",
+      "var += 1",
+      "var = 0");
+
+    TargetExpression target = buildFile
+      .findChildByClass(AssignmentStatement.class)
+      .getLeftHandSideExpression();
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(2);
+
+    assertThat(references[0]).isInstanceOf(LocalReference.class);
+    assertThat(references[1]).isInstanceOf(TargetReference.class);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilderTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilderTest.java
new file mode 100644
index 0000000..97ade21
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/formatting/BuildFileFoldingBuilderTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.formatting;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
+import com.intellij.lang.folding.FoldingDescriptor;
+import com.intellij.openapi.editor.Editor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for {@link BuildFileFoldingBuilder}.
+ */
+public class BuildFileFoldingBuilderTest extends BuildFileIntegrationTestCase {
+
+  public void testEndOfFileFunctionDelcaration() {
+    // bug 28618935: test no NPE in the case where there's no statement list following the func-def colon
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "def function():");
+
+    getFoldingRegions(file);
+  }
+
+  public void testFuncDefStatementsFolded() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "# multi-line comment, not folded",
+      "# second line of comment",
+      "def function(arg1, arg2):",
+      "    stmt1",
+      "    stmt2",
+      "",
+      "variable = 1");
+
+    FoldingDescriptor[] foldingRegions = getFoldingRegions(file);
+    assertThat(foldingRegions).hasLength(1);
+    assertThat(foldingRegions[0].getElement().getPsi())
+      .isEqualTo(file.findFunctionInScope("function"));
+  }
+
+  public void testRulesFolded() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(",
+      "    name = 'lib',",
+      "    srcs = glob(['*.java']),",
+      ")");
+
+    FoldingDescriptor[] foldingRegions = getFoldingRegions(file);
+    assertThat(foldingRegions).hasLength(1);
+    assertThat(foldingRegions[0].getElement().getPsi())
+      .isEqualTo(file.findRule("lib"));
+  }
+
+  public void testLoadStatementFolded() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "   '//java/com/foo/build_defs.bzl',",
+      "   'function1',",
+      "   'function2',",
+      ")");
+
+    FoldingDescriptor[] foldingRegions = getFoldingRegions(file);
+    assertThat(foldingRegions).hasLength(1);
+    assertThat(foldingRegions[0].getElement().getPsi())
+      .isEqualTo(file.findChildByClass(LoadStatement.class));
+  }
+
+  private FoldingDescriptor[] getFoldingRegions(BuildFile file) {
+    Editor editor = openFileInEditor(file.getVirtualFile());
+    return new BuildFileFoldingBuilder().buildFoldRegions(file.getNode(), editor.getDocument());
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.java
new file mode 100644
index 0000000..b53d30e
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/language/BuildFileTypeTest.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.lang.buildfile.language;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.psi.PsiFile;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that BUILD files are recognized as such
+ */
+public class BuildFileTypeTest extends BuildFileIntegrationTestCase {
+
+  public void testSkylarkExtensionRecognized() {
+    PsiFile file = createPsiFile("java/com/google/foo/build_defs.bzl");
+    assertThat(file).isInstanceOf(BuildFile.class);
+  }
+
+  public void testExactNameMatch() {
+    PsiFile file = createPsiFile("java/com/google/foo/BUILD");
+    assertThat(file).isInstanceOf(BuildFile.class);
+  }
+
+  /**
+   * We may want to support these in the future (and in the meantime the user can manually have them recognized as BUILD files,
+   * for syntax highlighting, etc.).<br>
+   * Currently, turned off by default because references won't resolve correctly -- they'll point back to normal BUILD files.
+   */
+  public void testOtherBuildFilesNotRecognized() {
+    PsiFile file = createPsiFile("java/com/google/foo/BUILD.tools");
+    assertThat(file).isNotInstanceOf(BuildFile.class);
+
+    file = createPsiFile("java/com/google/foo/BUILD.bazel");
+    assertThat(file).isNotInstanceOf(BuildFile.class);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java
new file mode 100644
index 0000000..5c16e80
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/AbstractLexerTest.java
@@ -0,0 +1,271 @@
+/*
+ * 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.lexer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests of tokenization behavior of {@link BuildLexerBase}.
+ */
+@RunWith(JUnit4.class)
+public abstract class AbstractLexerTest {
+
+  private final BuildLexerBase.LexerMode mode;
+  protected String lastError;
+
+  protected AbstractLexerTest(BuildLexerBase.LexerMode mode) {
+    this.mode = mode;
+  }
+
+  /**
+   * Create a lexer which takes input from the specified string. Resets the
+   * error handler beforehand.
+   */
+  protected BuildLexerBase createLexer(String input) {
+    lastError = null;
+    return new BuildLexerBase(input, 0, mode) {
+      @Override
+      protected void error(String message, int start, int end) {
+        super.error(message, start, end);
+        lastError = message;
+      }
+    };
+  }
+
+  protected Token[] tokens(String input) {
+    Token[] tokens = createLexer(input).getTokens().toArray(new Token[0]);
+    assertNoCharactersMissing(input.length(), tokens);
+    return tokens;
+  }
+
+  /**
+   * Both the syntax highlighter and the parser require every character be accounted for by
+   * a lexical element.
+   */
+  private static void assertNoCharactersMissing(int totalLength, Token[] tokens) {
+    if (tokens.length != 0 && tokens[tokens.length - 1].right != totalLength) {
+      throw new AssertionError(String.format(
+        "Last tokenized character '%s' doesn't match document length '%s'",
+        tokens[tokens.length - 1].right, totalLength));
+    }
+    int start = 0;
+    for (int i = 0; i < tokens.length; i++) {
+      Token token = tokens[i];
+      if (token.left != start) {
+        throw new AssertionError("Gap/inconsistency at: " + start);
+      }
+      start = token.right;
+    }
+  }
+
+  /**
+   * Returns a string containing the names of the tokens and their associated
+   * values. (String-literals are printed without escaping.)
+   */
+  protected String values(Token[] tokens) {
+    StringBuilder buffer = new StringBuilder();
+    for (Token token : tokens) {
+      if (isIgnored(token.kind)) {
+        continue;
+      }
+      if (buffer.length() > 0) {
+        buffer.append(' ');
+      }
+      buffer.append(token.kind.name());
+      if (token.kind != TokenKind.WHITESPACE && token.value != null) {
+        buffer.append('(').append(token.value).append(')');
+      }
+    }
+    return buffer.toString();
+  }
+
+  /**
+   * Returns a string containing just the names of the tokens.
+   */
+  protected String names(Token[] tokens) {
+    StringBuilder buf = new StringBuilder();
+    for (Token token : tokens) {
+      if (isIgnored(token.kind)) {
+        continue;
+      }
+      if (buf.length() > 0) {
+        buf.append(' ');
+      }
+      buf.append(token.kind.name());
+    }
+    return buf.toString();
+  }
+
+  private boolean isIgnored(TokenKind kind) {
+    if (mode == BuildLexerBase.LexerMode.Parsing) {
+      return kind == TokenKind.WHITESPACE || kind == TokenKind.COMMENT;
+    }
+    return false;
+  }
+
+  /**
+   * Returns a string containing just the half-open position intervals of each
+   * token. e.g. "[3,4) [4,9)".
+   */
+  protected String positions(Token[] tokens) {
+    StringBuilder buf = new StringBuilder();
+    for (Token token : tokens) {
+      if (isIgnored(token.kind)) {
+        continue;
+      }
+      if (buf.length() > 0) {
+        buf.append(' ');
+      }
+      buf.append('[')
+        .append(token.left)
+        .append(',')
+        .append(token.right)
+        .append(')');
+    }
+    return buf.toString();
+  }
+
+  @Test
+  public void testIntegers() throws Exception {
+    // Detection of MINUS immediately following integer constant proves we
+    // don't consume too many chars.
+
+    // decimal
+    assertEquals("INT(12345) MINUS", values(tokens("12345-")));
+
+    // octal
+    assertEquals("INT(5349) MINUS", values(tokens("012345-")));
+
+    // octal (bad)
+    assertEquals("INT(0) MINUS", values(tokens("012349-")));
+    assertEquals("invalid base-8 integer constant: 012349", lastError);
+
+    // hexadecimal (uppercase)
+    assertEquals("INT(1193055) MINUS", values(tokens("0X12345F-")));
+
+    // hexadecimal (lowercase)
+    assertEquals("INT(1193055) MINUS", values(tokens("0x12345f-")));
+
+    // hexadecimal (lowercase) [note: "g" cause termination of token]
+    assertEquals("INT(74565) IDENTIFIER(g) MINUS",
+      values(tokens("0x12345g-")));
+  }
+
+  @Test
+  public void testStringDelimiters() throws Exception {
+    assertEquals("STRING(foo)", values(tokens("\"foo\"")));
+    assertEquals("STRING(foo)", values(tokens("'foo'")));
+  }
+
+  @Test
+  public void testQuotesInStrings() throws Exception {
+    assertEquals("STRING(foo'bar)", values(tokens("'foo\\'bar'")));
+    assertEquals("STRING(foo'bar)", values(tokens("\"foo'bar\"")));
+    assertEquals("STRING(foo\"bar)", values(tokens("'foo\"bar'")));
+    assertEquals("STRING(foo\"bar)",
+      values(tokens("\"foo\\\"bar\"")));
+  }
+
+  @Test
+  public void testStringEscapes() throws Exception {
+    assertEquals("STRING(a\tb\nc\rd)",
+      values(tokens("'a\\tb\\nc\\rd'"))); // \t \r \n
+    assertEquals("STRING(x\\hx)",
+      values(tokens("'x\\hx'"))); // \h is unknown => "\h"
+    assertEquals("STRING(\\$$)", values(tokens("'\\$$'")));
+    assertEquals("STRING(ab)",
+      values(tokens("'a\\\nb'"))); // escape end of line
+    assertEquals("STRING(abcd)",
+      values(tokens("\"ab\\ucd\"")));
+    assertEquals("escape sequence not implemented: \\u", lastError);
+  }
+
+  @Test
+  public void testRawString() throws Exception {
+    assertEquals("STRING(abcd)",
+      values(tokens("r'abcd'")));
+    assertEquals("STRING(abcd)",
+      values(tokens("r\"abcd\"")));
+    assertEquals("STRING(a\\tb\\nc\\rd)",
+      values(tokens("r'a\\tb\\nc\\rd'"))); // r'a\tb\nc\rd'
+    assertEquals("STRING(a\\\")",
+      values(tokens("r\"a\\\"\""))); // r"a\""
+    assertEquals("STRING(a\\\\b)",
+      values(tokens("r'a\\\\b'"))); // r'a\\b'
+    assertEquals("STRING(ab) IDENTIFIER(r)",
+      values(tokens("r'ab'r")));
+
+    // Unterminated raw string
+    values(tokens("r'\\'")); // r'\'
+    assertEquals("unterminated string literal at eof", lastError);
+  }
+
+  @Test
+  public void testTripleRawString() throws Exception {
+    // r'''a\ncd'''
+    assertEquals("STRING(ab\\ncd)", values(tokens("r'''ab\\ncd'''")));
+    // r"""ab
+    // cd"""
+    assertEquals(
+      "STRING(ab\ncd)",
+      values(tokens("\"\"\"ab\ncd\"\"\"")));
+
+    // Unterminated raw string
+    values(tokens("r'''\\'''")); // r'''\'''
+    assertEquals("unterminated string literal at eof", lastError);
+  }
+
+  @Test
+  public void testOctalEscapes() throws Exception {
+    // Regression test for a bug.
+    assertEquals("STRING(\0 \1 \t \u003f I I1 \u00ff \u00ff \u00fe)",
+      values(tokens("'\\0 \\1 \\11 \\77 \\111 \\1111 \\377 \\777 \\776'")));
+    // Test boundaries (non-octal char, EOF).
+    assertEquals("STRING(\1b \1)", values(tokens("'\\1b \\1'")));
+  }
+
+  @Test
+  public void testTripleQuotedStrings() throws Exception {
+    assertEquals("STRING(a\"b'c \n d\"\"e)",
+      values(tokens("\"\"\"a\"b'c \n d\"\"e\"\"\"")));
+    assertEquals("STRING(a\"b'c \n d\"\"e)",
+      values(tokens("'''a\"b'c \n d\"\"e'''")));
+  }
+
+  @Test
+  public void testBadChar() throws Exception {
+    assertEquals("IDENTIFIER(a) ILLEGAL($) IDENTIFIER(b)", values(tokens("a$b")));
+    assertEquals("invalid character: '$'", lastError);
+  }
+
+  @Test
+  public void testContainsErrors() throws Exception {
+    BuildLexerBase lexerSuccess = createLexer("foo");
+    assertFalse(lexerSuccess.containsErrors());
+
+    BuildLexerBase lexerFail = createLexer("f$o");
+    assertTrue(lexerFail.containsErrors());
+
+    String s = "'unterminated";
+    lexerFail = createLexer(s);
+    assertTrue(lexerFail.containsErrors());
+    assertEquals("STRING(unterminated)", values(tokens(s)));
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/BlazeLexerTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/BlazeLexerTest.java
new file mode 100644
index 0000000..6d0b2e1
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/BlazeLexerTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.lexer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests of tokenization behavior of {@link BuildLexerBase} in 'parsing mode' (see {@link BuildLexerBase.LexerMode})
+ */
+@RunWith(JUnit4.class)
+public class BlazeLexerTest extends AbstractLexerTest {
+
+  public BlazeLexerTest() {
+    super(BuildLexerBase.LexerMode.Parsing);
+  }
+
+  @Test
+  public void testBasics1() throws Exception {
+    assertEquals("IDENTIFIER RPAREN", names(tokens("wiz) ")));
+    assertEquals("IDENTIFIER RPAREN", names(tokens("wiz )")));
+    assertEquals("IDENTIFIER RPAREN", names(tokens(" wiz)")));
+    assertEquals("IDENTIFIER RPAREN", names(tokens(" wiz ) ")));
+    assertEquals("IDENTIFIER RPAREN", names(tokens("wiz\t)")));
+  }
+
+  @Test
+  public void testBasics2() throws Exception {
+    assertEquals("RPAREN", names(tokens(")")));
+    assertEquals("RPAREN", names(tokens(" )")));
+    assertEquals("RPAREN", names(tokens(" ) ")));
+    assertEquals("RPAREN", names(tokens(") ")));
+  }
+
+  @Test
+  public void testBasics3() throws Exception {
+    assertEquals("INT NEWLINE INT", names(tokens("123#456\n789")));
+    assertEquals("INT NEWLINE INT", names(tokens("123 #456\n789")));
+    assertEquals("INT NEWLINE INT", names(tokens("123#456 \n789")));
+    assertEquals("INT NEWLINE INDENT INT", names(tokens("123#456\n 789")));
+    assertEquals("INT NEWLINE INT", names(tokens("123#456\n789 ")));
+  }
+
+  @Test
+  public void testBasics4() throws Exception {
+    assertEquals("", names(tokens("")));
+    assertEquals("", names(tokens("# foo")));
+    assertEquals("INT INT INT INT", names(tokens("1 2 3 4")));
+    assertEquals("INT DOT INT", names(tokens("1.234")));
+    assertEquals("IDENTIFIER LPAREN IDENTIFIER COMMA IDENTIFIER RPAREN", names(tokens("foo(bar, wiz)")));
+  }
+
+  @Test
+  public void testIntegersAndDot() throws Exception {
+    assertEquals("INT(1) DOT INT(2345)", values(tokens("1.2345")));
+
+    assertEquals("INT(1) DOT INT(2) DOT INT(345)", values(tokens("1.2.345")));
+
+    assertEquals("INT(1) DOT INT(0)", values(tokens("1.23E10")));
+    assertEquals("invalid base-10 integer constant: 23E10", lastError);
+
+    assertEquals("INT(1) DOT INT(0) MINUS INT(10)", values(tokens("1.23E-10")));
+    assertEquals("invalid base-10 integer constant: 23E", lastError);
+
+    assertEquals("DOT INT(123)", values(tokens(". 123")));
+    assertEquals("DOT INT(123)", values(tokens(".123")));
+    assertEquals("DOT IDENTIFIER(abc)", values(tokens(".abc")));
+
+    assertEquals("IDENTIFIER(foo) DOT INT(123)", values(tokens("foo.123")));
+    assertEquals("IDENTIFIER(foo) DOT IDENTIFIER(bcd)", values(tokens("foo.bcd"))); // 'b' are hex chars
+    assertEquals("IDENTIFIER(foo) DOT IDENTIFIER(xyz)", values(tokens("foo.xyz")));
+  }
+
+  @Test
+  public void testIndentation() throws Exception {
+    assertEquals("INT(1) NEWLINE INT(2) NEWLINE INT(3)",
+      values(tokens("1\n2\n3")));
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INT(3) NEWLINE DEDENT INT(4)",
+      values(tokens("1\n  2\n  3\n4 ")));
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INT(3)",
+      values(tokens("1\n  2\n  3")));
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3)",
+      values(tokens("1\n  2\n    3")));
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3) NEWLINE "
+        + "DEDENT INT(4) NEWLINE DEDENT INT(5)",
+      values(tokens("1\n  2\n    3\n  4\n5")));
+
+    assertEquals("INT(1) NEWLINE INDENT INT(2) NEWLINE INDENT INT(3) NEWLINE "
+        + "DEDENT INT(4) NEWLINE DEDENT INT(5)",
+      values(tokens("1\n  2\n    3\n   4\n5")));
+    assertEquals("indentation error", lastError);
+  }
+
+  @Test
+  public void testIndentationInsideParens() throws Exception {
+    // Indentation is ignored inside parens:
+    assertEquals("INT(1) LPAREN INT(2) INT(3) INT(4) INT(5)",
+      values(tokens("1 (\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) LBRACE INT(2) INT(3) INT(4) INT(5)",
+      values(tokens("1 {\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) LBRACKET INT(2) INT(3) INT(4) INT(5)",
+      values(tokens("1 [\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) LBRACKET INT(2) RBRACKET NEWLINE INDENT INT(3) "
+        + "NEWLINE INT(4) NEWLINE DEDENT INT(5)",
+      values(tokens("1 [\n  2]\n    3\n    4\n5")));
+  }
+
+  @Test
+  public void testNoIndentationAtEOF() throws Exception {
+    assertEquals("INDENT INT(1)", values(tokens("\n  1")));
+  }
+
+  @Test
+  public void testBlankLineIndentation() throws Exception {
+    // Blank lines and comment lines should not generate any indents
+    // (but note that every input ends with).
+    assertEquals("", names(tokens("\n      #\n")));
+    assertEquals("", names(tokens("      #")));
+    assertEquals("NEWLINE", names(tokens("      #\n")));
+    assertEquals("NEWLINE", names(tokens("      #comment\n")));
+    assertEquals("DEF IDENTIFIER LPAREN IDENTIFIER RPAREN COLON NEWLINE "
+        + "INDENT RETURN IDENTIFIER NEWLINE DEDENT",
+      names(tokens("def f(x):\n"
+        + "  # comment\n"
+        + "\n"
+        + "  \n"
+        + "  return x\n")));
+  }
+
+  @Test
+  public void testMultipleCommentLines() throws Exception {
+    assertEquals("NEWLINE "
+        + "DEF IDENTIFIER LPAREN IDENTIFIER RPAREN COLON NEWLINE "
+        + "INDENT RETURN IDENTIFIER NEWLINE DEDENT",
+      names(tokens("# Copyright\n"
+        + "#\n"
+        + "# A comment line\n"
+        + "# An adjoining line\n"
+        + "def f(x):\n"
+        + "  return x\n")));
+  }
+
+  @Test
+  public void testBackslash() throws Exception {
+    // backslash followed by newline marked as whitespace (skipped by parser)
+    assertEquals("IDENTIFIER IDENTIFIER",
+      names(tokens("a\\\nb")));
+    assertEquals("IDENTIFIER ILLEGAL IDENTIFIER",
+      names(tokens("a\\ b")));
+    assertEquals("IDENTIFIER LPAREN INT RPAREN",
+      names(tokens("a(\\\n2)")));
+  }
+
+  @Test
+  public void testTokenPositions() throws Exception {
+    //            foo   (     bar   ,     {      1       :
+    assertEquals("[0,3) [3,4) [4,7) [7,8) [9,10) [10,11) [11,12)"
+        //      'quux'  }       )
+        + " [13,19) [19,20) [20,21)",
+      positions(tokens("foo(bar, {1: 'quux'})")));
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java
new file mode 100644
index 0000000..40e91fd
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/lexer/HighlightingLexerTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.lexer;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests of tokenization behavior of {@link BuildLexerBase} in 'highlighting mode' (see {@link BuildLexerBase.LexerMode})
+ */
+@RunWith(JUnit4.class)
+public class HighlightingLexerTest extends AbstractLexerTest {
+
+  public HighlightingLexerTest() {
+    super(BuildLexerBase.LexerMode.SyntaxHighlighting);
+  }
+
+  @Test
+  public void testBasics1() throws Exception {
+    assertEquals("IDENTIFIER RPAREN WHITESPACE", names(tokens("wiz) ")));
+    assertEquals("IDENTIFIER WHITESPACE RPAREN", names(tokens("wiz )")));
+    assertEquals("WHITESPACE IDENTIFIER RPAREN", names(tokens(" wiz)")));
+    assertEquals("WHITESPACE IDENTIFIER WHITESPACE RPAREN WHITESPACE", names(tokens(" wiz ) ")));
+    assertEquals("IDENTIFIER WHITESPACE RPAREN", names(tokens("wiz\t)")));
+  }
+
+  @Test
+  public void testBasics2() throws Exception {
+    assertEquals("RPAREN", names(tokens(")")));
+    assertEquals("WHITESPACE RPAREN", names(tokens(" )")));
+    assertEquals("WHITESPACE RPAREN WHITESPACE", names(tokens(" ) ")));
+    assertEquals("RPAREN WHITESPACE", names(tokens(") ")));
+  }
+
+  @Test
+  public void testBasics3() throws Exception {
+    assertEquals("INT COMMENT NEWLINE INT", names(tokens("123#456\n789")));
+    assertEquals("INT WHITESPACE COMMENT NEWLINE INT", names(tokens("123 #456\n789")));
+    assertEquals("INT COMMENT NEWLINE INT", names(tokens("123#456 \n789")));
+    assertEquals("INT COMMENT NEWLINE WHITESPACE INT", names(tokens("123#456\n 789")));
+    assertEquals("INT COMMENT NEWLINE INT WHITESPACE", names(tokens("123#456\n789 ")));
+  }
+
+  @Test
+  public void testBasics4() throws Exception {
+    assertEquals("", names(tokens("")));
+    assertEquals("COMMENT", names(tokens("# foo")));
+    assertEquals("INT WHITESPACE INT WHITESPACE INT WHITESPACE INT", names(tokens("1 2 3 4")));
+    assertEquals("INT DOT INT", names(tokens("1.234")));
+    assertEquals("IDENTIFIER LPAREN IDENTIFIER COMMA WHITESPACE IDENTIFIER RPAREN", names(tokens("foo(bar, wiz)")));
+  }
+
+  @Test
+  public void testIntegersAndDot() throws Exception {
+    assertEquals("INT(1) DOT INT(2345)", values(tokens("1.2345")));
+
+    assertEquals("INT(1) DOT INT(2) DOT INT(345)", values(tokens("1.2.345")));
+
+    assertEquals("INT(1) DOT INT(0)", values(tokens("1.23E10")));
+    assertEquals("invalid base-10 integer constant: 23E10", lastError);
+
+    assertEquals("INT(1) DOT INT(0) MINUS INT(10)", values(tokens("1.23E-10")));
+    assertEquals("invalid base-10 integer constant: 23E", lastError);
+
+    assertEquals("DOT WHITESPACE INT(123)", values(tokens(". 123")));
+    assertEquals("DOT INT(123)", values(tokens(".123")));
+    assertEquals("DOT IDENTIFIER(abc)", values(tokens(".abc")));
+
+    assertEquals("IDENTIFIER(foo) DOT INT(123)", values(tokens("foo.123")));
+    assertEquals("IDENTIFIER(foo) DOT IDENTIFIER(bcd)", values(tokens("foo.bcd"))); // 'b' are hex chars
+    assertEquals("IDENTIFIER(foo) DOT IDENTIFIER(xyz)", values(tokens("foo.xyz")));
+  }
+
+  @Test
+  public void testNoIndentation() throws Exception {
+    assertEquals("INT(1) NEWLINE INT(2) NEWLINE INT(3)",
+      values(tokens("1\n2\n3")));
+    assertEquals("INT(1) NEWLINE WHITESPACE INT(2) NEWLINE WHITESPACE INT(3) NEWLINE INT(4) WHITESPACE",
+      values(tokens("1\n  2\n  3\n4 ")));
+    assertEquals("INT(1) NEWLINE WHITESPACE INT(2) NEWLINE WHITESPACE INT(3)",
+      values(tokens("1\n  2\n  3")));
+    assertEquals("INT(1) NEWLINE WHITESPACE INT(2) NEWLINE WHITESPACE INT(3)",
+      values(tokens("1\n  2\n    3")));
+    assertEquals("INT(1) NEWLINE WHITESPACE INT(2) NEWLINE WHITESPACE INT(3) NEWLINE WHITESPACE INT(4) NEWLINE INT(5)",
+      values(tokens("1\n  2\n    3\n  4\n5")));
+
+    assertEquals("INT(1) NEWLINE WHITESPACE INT(2) NEWLINE WHITESPACE INT(3) NEWLINE WHITESPACE INT(4) NEWLINE INT(5)",
+      values(tokens("1\n  2\n    3\n   4\n5")));
+  }
+
+  @Test
+  public void testIndentationInsideParens() throws Exception {
+    // Indentation is ignored inside parens:
+    assertEquals("INT(1) WHITESPACE LPAREN NEWLINE WHITESPACE INT(2) NEWLINE " +
+      "WHITESPACE INT(3) NEWLINE WHITESPACE INT(4) NEWLINE INT(5)",
+      values(tokens("1 (\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) WHITESPACE LBRACE NEWLINE WHITESPACE INT(2) NEWLINE " +
+      "WHITESPACE INT(3) NEWLINE WHITESPACE INT(4) NEWLINE INT(5)",
+      values(tokens("1 {\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) WHITESPACE LBRACKET NEWLINE WHITESPACE INT(2) NEWLINE " +
+      "WHITESPACE INT(3) NEWLINE WHITESPACE INT(4) NEWLINE INT(5)",
+      values(tokens("1 [\n  2\n    3\n  4\n5")));
+    assertEquals("INT(1) WHITESPACE LBRACKET NEWLINE WHITESPACE INT(2) RBRACKET " +
+      "NEWLINE WHITESPACE INT(3) NEWLINE WHITESPACE INT(4) NEWLINE INT(5)",
+      values(tokens("1 [\n  2]\n    3\n    4\n5")));
+  }
+
+  @Test
+  public void testNoIndentationAtEOF() throws Exception {
+    assertEquals("NEWLINE WHITESPACE INT(1)", values(tokens("\n  1")));
+  }
+
+  @Test
+  public void testBlankLineIndentation() throws Exception {
+    // Blank lines and comment lines should not generate any newlines indents
+    // (but note that every input ends with).
+    assertEquals("NEWLINE WHITESPACE COMMENT NEWLINE", names(tokens("\n      #\n")));
+    assertEquals("WHITESPACE COMMENT", names(tokens("      #")));
+    assertEquals("WHITESPACE COMMENT NEWLINE", names(tokens("      #\n")));
+    assertEquals("WHITESPACE COMMENT NEWLINE", names(tokens("      #comment\n")));
+    assertEquals("DEF WHITESPACE IDENTIFIER LPAREN IDENTIFIER RPAREN COLON NEWLINE WHITESPACE " +
+      "COMMENT NEWLINE NEWLINE WHITESPACE NEWLINE WHITESPACE RETURN WHITESPACE IDENTIFIER NEWLINE",
+      names(tokens("def f(x):\n"
+        + "  # comment\n"
+        + "\n"
+        + "  \n"
+        + "  return x\n")));
+  }
+
+  @Test
+  public void testMultipleCommentLines() throws Exception {
+    assertEquals("COMMENT NEWLINE COMMENT NEWLINE COMMENT NEWLINE COMMENT NEWLINE DEF WHITESPACE IDENTIFIER " +
+      "LPAREN IDENTIFIER RPAREN COLON NEWLINE WHITESPACE RETURN WHITESPACE IDENTIFIER NEWLINE",
+      names(tokens("# Copyright\n"
+        + "#\n"
+        + "# A comment line\n"
+        + "# An adjoining line\n"
+        + "def f(x):\n"
+        + "  return x\n")));
+  }
+
+  @Test
+  public void testBackslash() throws Exception {
+    // illegal characters marked as whitespace (skipped by parser)
+    assertEquals("IDENTIFIER WHITESPACE IDENTIFIER",
+      names(tokens("a\\\nb")));
+    assertEquals("IDENTIFIER ILLEGAL WHITESPACE IDENTIFIER",
+      names(tokens("a\\ b")));
+    assertEquals("IDENTIFIER LPAREN WHITESPACE INT RPAREN",
+      names(tokens("a(\\\n2)")));
+  }
+
+  @Test
+  public void testTokenPositions() throws Exception {
+    assertEquals("[0,3) [3,4) [4,7) [7,8) [8,9) [9,10) [10,11) [11,12) [12,13) [13,19) [19,20) [20,21)",
+      positions(tokens("foo(bar, {1: 'quux'})")));
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserTest.java
new file mode 100644
index 0000000..620bf24
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/parser/BuildParserTest.java
@@ -0,0 +1,577 @@
+/*
+ * 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.parser;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildElement;
+import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
+import com.intellij.lang.ASTNode;
+import com.intellij.lang.FileASTNode;
+import com.intellij.lang.ParserDefinition;
+import com.intellij.lang.PsiParser;
+import com.intellij.lang.impl.PsiBuilderAdapter;
+import com.intellij.lang.impl.PsiBuilderImpl;
+import com.intellij.lexer.Lexer;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.impl.source.CharTableImpl;
+import com.intellij.psi.impl.source.tree.LeafElement;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Test for the BUILD file parser (converting lexical elements into PSI elements)
+ */
+public class BuildParserTest extends BuildFileIntegrationTestCase {
+
+  private final List<String> errors = Lists.newArrayList();
+
+  @Override
+  protected void doTearDown() {
+    errors.clear();
+  }
+
+  public void testAugmentedAssign() throws Exception {
+    assertThat(parse("x += 1"))
+      .isEqualTo("aug_assign(reference, int)");
+    assertThat(parse("x -= 1"))
+      .isEqualTo("aug_assign(reference, int)");
+    assertThat(parse("x *= 1"))
+      .isEqualTo("aug_assign(reference, int)");
+    assertThat(parse("x /= 1"))
+      .isEqualTo("aug_assign(reference, int)");
+    assertThat(parse("x %= 1"))
+      .isEqualTo("aug_assign(reference, int)");
+    assertNoErrors();
+  }
+
+  public void testAssign() throws Exception {
+    assertThat(parse("a, b = 5\n"))
+      .isEqualTo("assignment(list(reference, target), int)");
+    assertNoErrors();
+  }
+
+  public void testAssign2() throws Exception {
+    assertThat(parse("a = b;c = d\n"))
+      .isEqualTo(Joiner.on("").join(
+        "assignment(target, reference), ",
+        "assignment(target, reference)"));
+    assertNoErrors();
+  }
+
+  public void testInvalidAssign() throws Exception {
+    parse("1 + (b = c)");
+    assertContainsErrors();
+  }
+
+  public void testTupleAssign() throws Exception {
+    assertThat(parse("list[0] = 5; dict['key'] = value\n"))
+      .isEqualTo(Joiner.on("").join(
+        "assignment(function_call(reference, positional(int)), int), ",
+        "assignment(function_call(reference, positional(string)), reference)"));
+    assertNoErrors();
+  }
+
+  public void testPrimary() throws Exception {
+    assertThat(parse("f(1 + 2)"))
+      .isEqualTo("function_call(reference, arg_list(positional(binary_op(int, int))))");
+    assertNoErrors();
+  }
+
+  public void testSecondary() throws Exception {
+    assertThat(parse("f(1 % 2)"))
+      .isEqualTo("function_call(reference, arg_list(positional(binary_op(int, int))))");
+    assertNoErrors();
+  }
+
+  public void testDoesNotGetStuck() throws Exception {
+    // Make sure the parser does not get stuck when trying
+    // to parse an expression containing a syntax error.
+    parse("f(1, ], 3)");
+    parse("f(1, ), 3)");
+    parse("[ ) for v in 3)");
+    parse("f(1, [x for foo foo foo foo], 3)");
+  }
+
+  public void testInvalidFunctionStatementDoesNotGetStuck() throws Exception {
+    // Make sure the parser does not get stuck when trying
+    // to parse a function statement containing a syntax error.
+    parse("def is ");
+    parse("def fn(");
+    parse("def empty)");
+  }
+
+  public void testSubstring() throws Exception {
+    assertThat(parse("'FOO.CC'[:].lower()[1:]"))
+      .isEqualTo(Joiner.on("").join(
+        "function_call(",
+        "function_call(function_call(string), reference, arg_list), ",
+        "positional(int))"));
+    assertNoErrors();
+  }
+
+  public void testFuncallExpr() throws Exception {
+    assertThat(parse("foo(1, 2, bar=wiz)"))
+      .isEqualTo(Joiner.on("").join(
+        "function_call(reference, arg_list(",
+        "positional(int), ",
+        "positional(int), ",
+        "keyword(reference)))"));
+    assertNoErrors();
+  }
+
+  public void testMethCallExpr() throws Exception {
+    assertThat(parse("foo.foo(1, 2, bar=wiz)"))
+      .isEqualTo(Joiner.on("").join(
+        "function_call(reference, reference, ",
+        "arg_list(positional(int), positional(int), keyword(reference)))"));
+    assertNoErrors();
+  }
+
+  public void testChainedMethCallExpr() throws Exception {
+    assertThat(parse("foo.replace().split(1)"))
+      .isEqualTo("function_call(function_call(reference, reference, arg_list), reference, arg_list(positional(int)))");
+    assertNoErrors();
+  }
+
+  public void testPropRefExpr() throws Exception {
+    assertThat(parse("foo.foo"))
+      .isEqualTo("dot_expr(reference, reference)");
+    assertNoErrors();
+  }
+
+  public void testStringMethExpr() throws Exception {
+    assertThat(parse("'foo'.foo()"))
+      .isEqualTo("function_call(string, reference, arg_list)");
+    assertNoErrors();
+  }
+
+  public void testFuncallLocation() throws Exception {
+    assertThat(parse("a(b);c = d\n"))
+      .isEqualTo(Joiner.on("").join(
+        "function_call(reference, arg_list(positional(reference))), ",
+        "assignment(target, reference)"));
+    assertNoErrors();
+  }
+
+  public void testList() throws Exception {
+    assertThat(parse("[0,f(1),2]"))
+      .isEqualTo("list(int, function_call(reference, arg_list(positional(int))), int)");
+    assertNoErrors();
+  }
+
+  public void testDict() throws Exception {
+    assertThat(parse("{1:2,2:f(1),3:4}"))
+      .isEqualTo(Joiner.on("").join(
+        "dict(",
+        "dict_entry(int, int), ",
+        "dict_entry(int, function_call(reference, arg_list(positional(int)))), ",
+        "dict_entry(int, int)",
+        ")"));
+    assertNoErrors();
+  }
+
+  public void testArgumentList() throws Exception {
+    assertThat(parse("f(0,g(1,2),2)"))
+      .isEqualTo(Joiner.on("").join(
+        "function_call(reference, arg_list(",
+        "positional(int), ",
+        "positional(function_call(reference, arg_list(positional(int), positional(int)))), ",
+        "positional(int)))"));
+    assertNoErrors();
+  }
+
+  public void testForBreakContinue() throws Exception {
+    String parsed = parse(
+      "def foo():",
+      "  for i in [1, 2]:",
+      "    break",
+      "    continue",
+      "    break");
+    assertThat(parsed)
+      .isEqualTo(Joiner.on("").join(
+        "function_def(parameter_list, ",
+        "stmt_list(for(target, list(int, int), ",
+        "stmt_list(flow, flow, flow))))"));
+    assertNoErrors();
+  }
+
+  public void testEmptyTuple() throws Exception {
+    assertThat(parse("()"))
+      .isEqualTo("list");
+    assertNoErrors();
+  }
+
+  public void testTupleTrailingComma() throws Exception {
+    assertThat(parse("(42,)"))
+      .isEqualTo("list(int)");
+    assertNoErrors();
+  }
+
+  public void testSingleton() throws Exception {
+    assertThat(parse("(42)")) // not a tuple!
+      .isEqualTo("list(int)");
+    assertNoErrors();
+  }
+
+  public void testDictionaryLiterals() throws Exception {
+    assertThat(parse("{1:42}"))
+      .isEqualTo("dict(dict_entry(int, int))");
+    assertNoErrors();
+  }
+
+  public void testDictionaryLiterals1() throws Exception {
+    assertThat(parse("{}"))
+      .isEqualTo("dict");
+    assertNoErrors();
+  }
+
+  public void testDictionaryLiterals2() throws Exception {
+    assertThat(parse("{1:42,}"))
+      .isEqualTo("dict(dict_entry(int, int))");
+    assertNoErrors();
+  }
+
+  public void testDictionaryLiterals3() throws Exception {
+    assertThat(parse("{1:42,2:43,3:44}"))
+      .isEqualTo(Joiner.on("").join(
+        "dict(",
+        "dict_entry(int, int), ",
+        "dict_entry(int, int), ",
+        "dict_entry(int, int))"));
+    assertNoErrors();
+  }
+
+  public void testInvalidListComprehensionSyntax() throws Exception {
+    assertThat(parse("[x for x for y in ['a']]"))
+      .isEqualTo("list_comp(reference, reference)");
+    assertContainsErrors();
+  }
+
+  public void testListComprehensionEmptyList() throws Exception {
+    // At the moment, we just parse the components of comprehension suffixes.
+    assertThat(parse("['foo/%s.java' % x for x in []]"))
+      .isEqualTo("list_comp(binary_op(string, reference), target, list)");
+    assertNoErrors();
+  }
+
+  public void testListComprehension() throws Exception {
+    assertThat(parse("['foo/%s.java' % x for x in ['bar', 'wiz', 'quux']]"))
+      .isEqualTo(Joiner.on("").join(
+        "list_comp(binary_op(string, reference), ",
+        "target, ",
+        "list(string, string, string))"));
+    assertNoErrors();
+  }
+
+  public void testDoesntGetStuck2() throws Exception {
+    parse(
+      "def foo():",
+      "  a = 2 for 4",  // parse error
+      "  b = [3, 4]",
+      "",
+      "d = 4 ada",  // parse error
+      "",
+      "def bar():",
+      "  a = [3, 4]",
+      "  b = 2 + + 5",  // parse error
+      "");
+    assertContainsErrors();
+  }
+
+  public void testDoesntGetStuck3() throws Exception {
+    parse("load(*)");
+    parse("load()");
+    parse("load(,)");
+    parse("load)");
+    parse("load(,");
+    parse("load(,\"string\"");
+    assertContainsErrors();
+  }
+
+  public void testExprAsStatement() throws Exception {
+    String parsed = parse(
+      "li = []",
+      "li.append('a.c')",
+      "\"\"\" string comment \"\"\"",
+      "foo(bar)");
+    assertThat(parsed)
+      .isEqualTo(Joiner.on("").join(
+        "assignment(target, list), ",
+        "function_call(reference, reference, arg_list(positional(string))), ",
+        "string, ",
+        "function_call(reference, arg_list(positional(reference)))"));
+    assertNoErrors();
+  }
+
+  public void testPrecedence1() {
+    assertThat(parse("'%sx' % 'foo' + 'bar'"))
+      .isEqualTo("binary_op(binary_op(string, string), string)");
+    assertNoErrors();
+  }
+
+  public void testPrecedence2() {
+    assertThat(parse("('%sx' + 'foo') * 'bar'"))
+      .isEqualTo("binary_op(list(binary_op(string, string)), string)");
+    assertNoErrors();
+  }
+
+  public void testPrecedence3() {
+    assertThat(parse("'%sx' % ('foo' + 'bar')"))
+      .isEqualTo("binary_op(string, list(binary_op(string, string)))");
+    assertNoErrors();
+  }
+
+  public void testPrecedence4() throws Exception {
+    assertThat(parse("1 + - (2 - 3)"))
+      .isEqualTo("binary_op(int, positional(list(binary_op(int, int))))");
+    assertNoErrors();
+  }
+
+  public void testPrecedence5() throws Exception {
+    assertThat(parse("2 * x | y + 1"))
+      .isEqualTo("binary_op(binary_op(int, reference), binary_op(reference, int))");
+    assertNoErrors();
+  }
+
+  public void testNotIsIgnored() throws Exception {
+    assertThat(parse("not 'b'"))
+      .isEqualTo("string");
+    assertNoErrors();
+  }
+
+  public void testNotIn() throws Exception {
+    assertThat(parse("'a' not in 'b'"))
+      .isEqualTo("binary_op(string, string)");
+    assertNoErrors();
+  }
+
+  public void testParseBuildFileWithSingeRule() throws Exception {
+    ASTNode tree = createAST(
+      "genrule(name = 'foo',",
+      "   srcs = ['input.csv'],",
+      "   outs = [ 'result.txt',",
+      "           'result.log'],",
+      "   cmd = 'touch result.txt result.log')");
+    List<BuildElement> stmts = getTopLevelNodesOfType(tree, BuildElement.class);
+    assertThat(stmts).hasSize(1);
+    assertNoErrors();
+  }
+
+  public void testParseBuildFileWithMultipleRules() throws Exception {
+    ASTNode tree = createAST(
+      "genrule(name = 'foo',",
+      "   srcs = ['input.csv'],",
+      "   outs = [ 'result.txt',",
+      "           'result.log'],",
+      "   srcs = ['input.csv'],",
+      "   cmd = 'touch result.txt result.log')",
+      "",
+      "genrule(name = 'bar',",
+      "   outs = [ 'graph.svg'],",
+      "   cmd = 'touch graph.svg')");
+    List<BuildElement> stmts = getTopLevelNodesOfType(tree, BuildElement.class);
+    assertThat(stmts).hasSize(2);
+    assertNoErrors();
+  }
+
+  public void testMissingComma() throws Exception {
+    // missing comma after name='foo'
+    parse("genrule(name = 'foo'",
+          "   srcs = ['in'])");
+    assertContainsError("',' expected");
+  }
+
+  public void testDoubleSemicolon() throws Exception {
+    parse("x = 1; ; x = 2;");
+    assertContainsError("expected an expression");
+  }
+
+  public void testMissingBlock() throws Exception {
+    parse(
+      "x = 1;",
+      "def foo(x):",
+      "x = 2;\n");
+    assertContainsError("'indent' expected");
+  }
+
+  public void testFunCallBadSyntax() throws Exception {
+    parse("f(1,\n");
+    assertContainsError("')' expected");
+  }
+
+  public void testFunCallBadSyntax2() throws Exception {
+    parse("f(1, 5, ,)\n");
+    assertContainsError("expected an expression");
+  }
+
+  public void testLoad() throws Exception {
+    ASTNode tree = createAST("load('file', 'foo', 'bar',)\n");
+    List<LoadStatement> stmts = getTopLevelNodesOfType(tree, LoadStatement.class);
+    assertThat(stmts).hasSize(1);
+
+    LoadStatement stmt = stmts.get(0);
+    assertThat(stmt.getImportedPath()).isEqualTo("file");
+    assertThat(stmt.getImportedSymbolNames()).isEqualTo(new String[] {"foo", "bar"});
+    assertNoErrors();
+  }
+
+  public void testLoadNoSymbol() throws Exception {
+    parse("load('/foo/bar/file')\n");
+    assertContainsError("'load' statements must include at least one loaded function");
+  }
+
+  public void testFunctionDefinition() throws Exception {
+    ASTNode tree = createAST(
+      "def function(name = 'foo', srcs, outs, *args, **kwargs):",
+      "   native.java_library(",
+      "     name = name,",
+      "     srcs = srcs,",
+      "   )",
+      "   return");
+    List<BuildElement> stmts = getTopLevelNodesOfType(tree, BuildElement.class);
+    assertThat(stmts).hasSize(1);
+    assertNoErrors();
+  }
+
+  public void testFunctionCall() throws Exception {
+    ASTNode tree = createAST("function(name = 'foo', srcs, *args, **kwargs)");
+    List<BuildElement> stmts = getTopLevelNodesOfType(tree, BuildElement.class);
+    assertThat(stmts).hasSize(1);
+    assertThat(treeToString(tree))
+      .isEqualTo(Joiner.on("").join(
+        "function_call(reference, arg_list(",
+        "keyword(string), ",
+        "positional(reference), ",
+        "*(reference), ",
+        "**(reference)))"));
+    assertNoErrors();
+  }
+
+  public void testConditionalStatement() throws Exception {
+    // we don't yet bother specifying which kind of conditionals we hit
+    assertThat(parse("if x : y elif a : b else c"))
+      .isEqualTo(Joiner.on("").join(
+        "if(",
+        "if_part(reference, reference), ",
+        "else_if_part(reference, reference), ",
+        "else_part(reference))"));
+  }
+
+  private ASTNode createAST(String... lines) {
+    StringBuilder builder = new StringBuilder();
+    for (String line : lines) {
+      builder.append(line).append("\n");
+    }
+    return createAST(builder.toString());
+  }
+
+  private ASTNode createAST(String text) {
+    ParserDefinition definition = new BuildParserDefinition();
+    PsiParser parser = definition.createParser(getProject());
+    Lexer lexer = definition.createLexer(getProject());
+    PsiBuilderImpl psiBuilder = new PsiBuilderImpl(getProject(), null, definition, lexer, new CharTableImpl(), text, null, null);
+    PsiBuilderAdapter adapter = new PsiBuilderAdapter(psiBuilder) {
+      @Override
+      public void error(String messageText) {
+        super.error(messageText);
+        errors.add(messageText);
+      }
+    };
+    return parser.parse(definition.getFileNodeType(), adapter);
+  }
+
+  private String parse(String... lines) {
+    StringBuilder builder = new StringBuilder();
+    for (String line : lines) {
+      builder.append(line).append("\n");
+    }
+    return parse(builder.toString());
+  }
+
+  private String parse(String text) {
+    ASTNode tree = createAST(text);
+    return treeToString(tree);
+  }
+
+  private String treeToString(ASTNode tree) {
+    StringBuilder builder = new StringBuilder();
+    nodeToString(tree, builder);
+    return builder.toString();
+  }
+
+  private void nodeToString(ASTNode node, StringBuilder builder) {
+    if (node instanceof LeafElement || node.getPsi() == null) {
+      return;
+    }
+    PsiElement[] childPsis = getChildBuildPsis(node);
+    if (node instanceof FileASTNode) {
+      appendChildren(childPsis, builder, false);
+      return;
+    }
+    builder.append(node.getElementType());
+    appendChildren(childPsis, builder, true);
+  }
+
+  private void appendChildren(PsiElement[] childPsis, StringBuilder builder, boolean bracket) {
+    if (childPsis.length == 0) {
+      return;
+    }
+    if (bracket) {
+      builder.append("(");
+    }
+    nodeToString(childPsis[0].getNode(), builder);
+    for (int i = 1; i < childPsis.length; i++) {
+      builder.append(", ");
+      nodeToString(childPsis[i].getNode(), builder);
+    }
+    if (bracket) {
+      builder.append(")");
+    }
+  }
+
+  private static <T> List<T> getTopLevelNodesOfType(ASTNode node, Class<T> clazz) {
+    return (List) Arrays.stream(node.getChildren(null))
+      .map(ASTNode::getPsi)
+      .filter(psiElement -> clazz.isInstance(psiElement))
+      .collect(Collectors.toList());
+  }
+
+  private PsiElement[] getChildBuildPsis(ASTNode node) {
+    return Arrays.stream(node.getChildren(null))
+      .map(ASTNode::getPsi)
+      .filter(psiElement -> psiElement instanceof BuildElement)
+      .toArray(PsiElement[]::new);
+  }
+
+  private void assertNoErrors() {
+    assertThat(errors).isEmpty();
+  }
+
+  private void assertContainsErrors() {
+    assertThat(errors).isNotEmpty();
+  }
+
+  private void assertContainsError(String message) {
+    assertThat(errors).contains(message);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/FileCopyTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/FileCopyTest.java
new file mode 100644
index 0000000..0039011
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/FileCopyTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.refactor;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.intellij.openapi.command.WriteCommandAction;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.refactoring.copy.CopyHandler;
+
+/**
+ * Tests copying files
+ */
+public class FileCopyTest extends BuildFileIntegrationTestCase {
+
+  public void testCopyingJavaFileReferencedByGlob() {
+    createDirectory("java");
+    PsiFile javaFile = createPsiFile(
+      "java/Test.java",
+      "package java;",
+      "public class Test {}");
+
+    PsiFile javaFile2 = createPsiFile(
+      "java/Test2.java",
+      "package java;",
+      "public class Test2 {}");
+
+    createBuildFile(
+      "java/BUILD",
+      "java_library(",
+      "    name = 'lib',",
+      "    srcs = glob(['**/*.java']),",
+      ")");
+
+    PsiDirectory otherDir = createPsiDirectory("java/other");
+
+    WriteCommandAction.runWriteCommandAction(null, () -> {
+      CopyHandler.doCopy(new PsiElement[] {javaFile, javaFile2}, otherDir);
+    });
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java
new file mode 100644
index 0000000..26e0030
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/refactor/RenameRefactoringTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.refactor;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.psi.PsiFile;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that BUILD file references are correctly updated when performing rename refactors.
+ */
+public class RenameRefactoringTest extends BuildFileIntegrationTestCase {
+
+  public void testRenameJavaClass() {
+    PsiFile javaFile = createPsiFile(
+      "com/google/foo/JavaClass.java",
+      "package com.google.foo;",
+      "public class JavaClass {}");
+
+    BuildFile buildFile = createBuildFile(
+      "com/google/foo/BUILD",
+      "java_library(name = \"ref1\", srcs = [\"//com/google/foo:JavaClass.java\"])",
+      "java_library(name = \"ref2\", srcs = [\"JavaClass.java\"])",
+      "java_library(name = \"ref3\", srcs = [\":JavaClass.java\"])");
+
+    List<StringLiteral> references = findAllReferencingElementsOfType(javaFile, StringLiteral.class);
+    assertThat(references).hasSize(3);
+
+    assertThat(references.get(0).getStringContents()).isEqualTo("//com/google/foo:JavaClass.java");
+    assertThat(references.get(1).getStringContents()).isEqualTo("JavaClass.java");
+    assertThat(references.get(2).getStringContents()).isEqualTo(":JavaClass.java");
+
+    renamePsiElement(javaFile, "NewName.java");
+    assertThat(references.get(0).getStringContents()).isEqualTo("//com/google/foo:NewName.java");
+    assertThat(references.get(1).getStringContents()).isEqualTo("NewName.java");
+    assertThat(references.get(2).getStringContents()).isEqualTo(":NewName.java");
+  }
+
+  public void testRenameRule() {
+    BuildFile fooPackage = createBuildFile(
+      "com/google/foo/BUILD",
+      "rule_type(name = \"target\")",
+      "java_library(name = \"local_ref\", srcs = [\":target\"])");
+
+    BuildFile barPackage = createBuildFile(
+      "com/google/test/bar/BUILD",
+      "rule_type(name = \"ref\", arg = \"//com/google/foo:target\")",
+      "top_level_ref = \"//com/google/foo:target\"");
+
+    FuncallExpression targetRule = PsiUtils.findFirstChildOfClassRecursive(fooPackage, FuncallExpression.class);
+    renamePsiElement(targetRule, "newTargetName");
+
+    assertFileContents(fooPackage,
+      "rule_type(name = \"newTargetName\")",
+      "java_library(name = \"local_ref\", srcs = [\":newTargetName\"])");
+
+    assertFileContents(barPackage,
+      "rule_type(name = \"ref\", arg = \"//com/google/foo:newTargetName\")",
+      "top_level_ref = \"//com/google/foo:newTargetName\"");
+  }
+
+  public void testRenameSkylarkExtension() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/tools/build_defs.bzl",
+      "def function(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google:tools/build_defs.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", deps = []");
+
+    renamePsiElement(extFile, "skylark.bzl");
+
+    assertFileContents(buildFile,
+      "load(",
+      "\"//java/com/google:tools/skylark.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", deps = []");
+  }
+
+  public void testRenameLoadedFunction() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/tools/build_defs.bzl",
+      "def function(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google/tools:build_defs.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", deps = []");
+
+    FunctionStatement fn = extFile.findChildByClass(FunctionStatement.class);
+    renamePsiElement(fn, "action");
+
+    assertFileContents(extFile,
+      "def action(name, deps)");
+
+    assertFileContents(buildFile,
+      "load(",
+      "\"//java/com/google/tools:build_defs.bzl\",",
+      "\"action\"",
+      ")",
+      "action(name = \"name\", deps = []");
+  }
+
+  public void testRenameLocalVariable() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "a = 1",
+      "c = a");
+
+    TargetExpression target = PsiUtils.findFirstChildOfClassRecursive(file, TargetExpression.class);
+    assertThat(target.getText()).isEqualTo("a");
+
+    renamePsiElement(target, "b");
+
+    assertFileContents(file,
+      "b = 1",
+      "c = b");
+  }
+
+  // all references, including path fragments in labels, should be renamed.
+  public void testRenameDirectory() {
+    BuildFile bazPackage = createBuildFile("java/com/baz/BUILD");
+    BuildFile toolsSubpackage = createBuildFile("java/com/google/tools/BUILD");
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google/tools:build_defs.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", deps = [\"//java/com/baz:target\"]");
+
+    renameDirectory("java/com", "java/alt");
+
+    assertFileContents(buildFile,
+      "load(",
+      "\"//java/alt/google/tools:build_defs.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", deps = [\"//java/alt/baz:target\"]");
+  }
+
+  public void testRenameFunctionParameter() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/tools/build_defs.bzl",
+      "def function(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google/tools:build_defs.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", deps = []");
+
+    FunctionStatement fn = extFile.findChildByClass(FunctionStatement.class);
+    Parameter param = fn.getParameterList().findParameterByName("deps");
+    renamePsiElement(param, "exports");
+
+    assertFileContents(extFile,
+      "def function(name, exports)");
+
+    assertFileContents(buildFile,
+      "load(",
+      "\"//java/com/google/tools:build_defs.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", exports = []");
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java
new file mode 100644
index 0000000..09ffe7e
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/GlobReferenceTest.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.GlobExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.ResolveResult;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that glob references are resolved correctly.
+ */
+public class GlobReferenceTest extends BuildFileIntegrationTestCase {
+
+  public void testSimpleGlobReferencingSingleFile() {
+    PsiFile ref = createPsiFile("java/com/google/Test.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).hasSize(1);
+    assertThat(references).containsExactly(ref);
+  }
+
+  public void testSimpleGlobReferencingSingleFile2() {
+    PsiFile ref = createPsiFile("java/com/google/Test.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).hasSize(1);
+    assertThat(references).containsExactly(ref);
+  }
+
+  public void testSimpleGlobReferencingSingleFile3() {
+    PsiFile ref = createPsiFile("java/com/google/Test.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['T*t.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).hasSize(1);
+    assertThat(references).containsExactly(ref);
+  }
+
+  public void testGlobReferencingMultipleFiles() {
+    PsiFile ref1 = createPsiFile("java/com/google/Test.java");
+    PsiFile ref2 = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).hasSize(2);
+    assertThat(references).containsExactly(ref1, ref2);
+  }
+
+  public void testFindsSubDirectories() {
+    PsiFile ref1 = createPsiFile("java/com/google/test/Test.java");
+    PsiFile ref2 = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).hasSize(2);
+    assertThat(references).containsExactly(ref1, ref2);
+  }
+
+  public void testGlobWithExcludes() {
+    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
+    PsiFile foo = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(" +
+      "  ['**/*.java']," +
+      "  exclude = ['tests/*.java'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).hasSize(1);
+    assertThat(references).containsExactly(foo);
+  }
+
+  public void testIncludeDirectories() {
+    createDirectory("java/com/google/tests");
+    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
+    PsiFile foo = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(" +
+      "  ['**/*']," +
+      "  exclude = ['BUILD']," +
+      "  exclude_directories = 0)");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).hasSize(3);
+    assertThat(references).containsExactly(foo, test, test.getParent());
+  }
+
+  public void testExcludeDirectories() {
+    createDirectory("java/com/google/tests");
+    PsiFile test = createPsiFile("java/com/google/tests/Test.java");
+    PsiFile foo = createPsiFile("java/com/google/Foo.java");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(" +
+      "  ['**/*']," +
+      "  exclude = ['BUILD'])");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(file, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).hasSize(2);
+    assertThat(references).containsExactly(foo, test);
+  }
+
+  public void testFilesInSubpackagesExcluded() {
+    BuildFile pkg = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'])");
+    BuildFile subPkg = createBuildFile("java/com/google/other/BUILD");
+    createFile("java/com/google/other/Other.java");
+
+    GlobExpression glob = PsiUtils.findFirstChildOfClassRecursive(pkg, GlobExpression.class);
+    List<PsiElement> references = multiResolve(glob);
+    assertThat(references).isEmpty();
+  }
+
+  private List<PsiElement> multiResolve(GlobExpression glob) {
+    ResolveResult[] result = glob.getReference().multiResolve(false);
+    return Arrays.stream(result)
+      .map(ResolveResult::getElement)
+      .filter(Objects::nonNull)
+      .collect(Collectors.toList());
+  }
+
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/KeywordReferenceTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/KeywordReferenceTest.java
new file mode 100644
index 0000000..da60773
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/KeywordReferenceTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that keyword references are correctly resolved.
+ */
+public class KeywordReferenceTest extends BuildFileIntegrationTestCase {
+
+  public void testPlainKeywordReference() {
+    BuildFile file = createBuildFile(
+      "java/com/google/build_defs.bzl",
+      "def function(name, deps)",
+      "function(name = \"name\", deps = [])");
+
+    ParameterList params = file.firstChildOfClass(FunctionStatement.class).getParameterList();
+    assertThat(params.getElements()).hasLength(2);
+
+    ArgumentList args = file.firstChildOfClass(FuncallExpression.class).getArgList();
+    assertThat(args.getKeywordArgument("name").getReferencedElement())
+      .isEqualTo(params.findParameterByName("name"));
+
+    assertThat(args.getKeywordArgument("deps").getReferencedElement())
+      .isEqualTo(params.findParameterByName("deps"));
+  }
+
+  public void testKwargsReference() {
+    BuildFile file = createBuildFile(
+      "java/com/google/build_defs.bzl",
+      "def function(name, **kwargs)",
+      "function(name = \"name\", deps = [])");
+
+    ArgumentList args = file.firstChildOfClass(FuncallExpression.class).getArgList();
+    assertThat(args.getKeywordArgument("deps").getReferencedElement()).isInstanceOf(Parameter.StarStar.class);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.java
new file mode 100644
index 0000000..09bdbff
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LabelReferenceTest.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.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiReference;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that string literal references are correctly resolved.
+ */
+public class LabelReferenceTest extends BuildFileIntegrationTestCase {
+
+  public void testExternalFileReference() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "exports_files([\"test.txt\", \"//java/com/google:plugin.xml\"])");
+
+    PsiFile txtFile = createPsiFile("java/com/google/test.txt");
+    PsiFile xmlFile = createPsiFile("java/com/google/plugin.xml");
+
+    List<StringLiteral> strings = PsiUtils.findAllChildrenOfClassRecursive(file, StringLiteral.class);
+    assertThat(strings).hasSize(2);
+    assertThat(strings.get(0).getReferencedElement())
+      .isEqualTo(txtFile);
+    assertThat(strings.get(1).getReferencedElement())
+      .isEqualTo(xmlFile);
+  }
+
+  public void testLocalRuleReference() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "java_library(name = \"lib\")",
+      "java_library(name = \"foo\", deps = [\":lib\"])",
+      "java_library(name = \"bar\", deps = [\"//java/com/google:lib\"])");
+
+    FuncallExpression lib = file.findRule("lib");
+    FuncallExpression foo = file.findRule("foo");
+    FuncallExpression bar = file.findRule("bar");
+
+    assertThat(lib).isNotNull();
+
+    StringLiteral label = PsiUtils.findFirstChildOfClassRecursive(foo.getKeywordArgument("deps"), StringLiteral.class);
+    assertThat(label.getReferencedElement()).isEqualTo(lib);
+
+    label = PsiUtils.findFirstChildOfClassRecursive(bar.getKeywordArgument("deps"), StringLiteral.class);
+    assertThat(label.getReferencedElement()).isEqualTo(lib);
+  }
+
+  public void testTargetInAnotherPackageResolves() {
+    BuildFile targetFile = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "rule(name = \"target\")");
+
+    BuildFile referencingFile = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "rule(name = \"other\", dep = \"//java/com/google/foo:target\")");
+
+    FuncallExpression target = targetFile.findRule("target");
+    assertThat(target).isNotNull();
+
+    Argument.Keyword depArgument = referencingFile
+      .findRule("other")
+      .getKeywordArgument("dep");
+
+    assertThat(depArgument.getValue().getReferencedElement())
+      .isEqualTo(target);
+  }
+
+  public void testRuleNameDoesntCrossPackageBoundaries() {
+    BuildFile targetFile = createBuildFile(
+      "java/com/google/pkg/subpkg/BUILD",
+      "rule(name = \"target\")");
+
+    BuildFile referencingFile = createBuildFile(
+      "java/com/google/pkg/BUILD",
+      "rule(name = \"other\", dep = \":subpkg/target\")");
+
+    Argument.Keyword depArgument = referencingFile
+      .findRule("other")
+      .getKeywordArgument("dep");
+
+    LabelReference ref = (LabelReference) depArgument.getValue().getReference();
+    assertThat(ref.resolve()).isNull();
+
+    replaceStringContents(ref.getElement(), "//java/com/google/pkg/subpkg:target");
+    assertThat(ref.resolve()).isNotNull();
+    assertThat(ref.resolve()).isEqualTo(targetFile.findRule("target"));
+  }
+
+  public void testLabelWithImplicitRuleName() {
+    BuildFile targetFile = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "rule(name = \"foo\")");
+
+    BuildFile referencingFile = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "rule(name = \"other\", dep = \"//java/com/google/foo\")");
+
+    FuncallExpression target = targetFile.findRule("foo");
+    assertThat(target).isNotNull();
+
+    Argument.Keyword depArgument = referencingFile
+      .findRule("other")
+      .getKeywordArgument("dep");
+
+    assertThat(depArgument.getValue().getReferencedElement())
+      .isEqualTo(target);
+  }
+
+  public void testAbsoluteLabelInSkylarkExtension() {
+    BuildFile targetFile = createBuildFile(
+      "java/com/google/foo/BUILD",
+      "rule(name = \"foo\")");
+
+    BuildFile referencingFile = createBuildFile(
+      "java/com/google/foo/skylark.bzl",
+      "LIST = ['//java/com/google/foo:foo']");
+
+    FuncallExpression target = targetFile.findRule("foo");
+    assertThat(target).isNotNull();
+
+    StringLiteral label = PsiUtils.findFirstChildOfClassRecursive(referencingFile, StringLiteral.class);
+    assertThat(label.getReferencedElement()).isEqualTo(target);
+  }
+
+  public void testRulePreferredOverFile() {
+    BuildFile targetFile = createBuildFile(
+      "java/com/foo/BUILD",
+      "java_library(name = 'lib')");
+
+    createDirectory("java/com/foo/lib");
+
+    BuildFile referencingFile = createBuildFile(
+      "java/com/google/bar/BUILD",
+      "java_library(",
+      "    name = 'bar',",
+      "    src = glob(['**/*.java'])," +
+      "    deps = ['//java/com/foo:lib'],",
+      ")");
+
+    FuncallExpression target = targetFile.findRule("lib");
+    assertThat(target).isNotNull();
+
+    PsiReference[] references = FindUsages.findAllReferences(target);
+    assertThat(references).hasLength(1);
+
+    PsiElement element = references[0].getElement();
+    FuncallExpression rule = PsiUtils.getParentOfType(element, FuncallExpression.class);
+    assertThat(rule.getName()).isEqualTo("bar");
+    assertThat(rule.getContainingFile()).isEqualTo(referencingFile);
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
new file mode 100644
index 0000000..d1c6356
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LoadedSkylarkExtensionTest.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.FunctionStatement;
+import com.google.idea.blaze.base.lang.buildfile.psi.LoadStatement;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that funcall references and load statement contents are correctly resolved.
+ */
+public class LoadedSkylarkExtensionTest extends BuildFileIntegrationTestCase {
+
+  public void testStandardLoadReference() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/build_defs.bzl",
+      "def function(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google:build_defs.bzl\",",
+      "\"function\"",
+      ")");
+
+    LoadStatement load = buildFile.firstChildOfClass(LoadStatement.class);
+    assertThat(load.getImportPsiElement().getReferencedElement()).isEqualTo(extFile);
+
+    FunctionStatement function = extFile.firstChildOfClass(FunctionStatement.class);
+    assertThat(function).isNotNull();
+
+    assertThat(load.getImportedSymbolElements()).hasLength(1);
+    assertThat(load.getImportedSymbolElements()[0].getReferencedElement()).isEqualTo(function);
+  }
+
+  // TODO: If we want to support this deprecated format, we should start by relaxing the ":" requirement in Label
+  //public void testDeprecatedImportLabelFormat() {
+  //  BuildFile extFile = createBuildFile(
+  //    "java/com/google/build_defs.bzl",
+  //    "def function(name, deps)");
+  //
+  //  BuildFile buildFile = createBuildFile(
+  //    "java/com/google/tools/BUILD",
+  //    "load(",
+  //    "\"//java/com/google/build_defs.bzl\",",
+  //    "\"function\"",
+  //    ")");
+  //
+  //  LoadStatement load = buildFile.firstChildOfClass(LoadStatement.class);
+  //  assertThat(load.getImportPsiElement().getReferencedElement()).isEqualTo(extFile);
+  //}
+
+  public void testPackageLocalImportLabelFormat() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/tools/build_defs.bzl",
+      "def function(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/tools/BUILD",
+      "load(",
+      "\":build_defs.bzl\",",
+      "\"function\"",
+      ")");
+
+    LoadStatement load = buildFile.firstChildOfClass(LoadStatement.class);
+    assertThat(load.getImportPsiElement().getReferencedElement()).isEqualTo(extFile);
+  }
+
+  public void testMultipleImportedFunctions() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/build_defs.bzl",
+      "def fn1(name, deps)",
+      "def fn2(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google:build_defs.bzl\",",
+      "\"fn1\"",
+      "\"fn2\"",
+      ")");
+
+    LoadStatement load = buildFile.firstChildOfClass(LoadStatement.class);
+    assertThat(load.getImportPsiElement().getReferencedElement()).isEqualTo(extFile);
+
+    FunctionStatement[] functions = extFile.childrenOfClass(FunctionStatement.class);
+    assertThat(functions).hasLength(2);
+    assertThat(load.getImportedFunctionReferences()).isEqualTo(functions);
+  }
+
+  public void testFuncallReference() {
+    BuildFile extFile = createBuildFile(
+      "java/com/google/tools/build_defs.bzl",
+      "def function(name, deps)");
+
+    BuildFile buildFile = createBuildFile(
+      "java/com/google/BUILD",
+      "load(",
+      "\"//java/com/google/tools:build_defs.bzl\",",
+      "\"function\"",
+      ")",
+      "function(name = \"name\", deps = []");
+
+    FunctionStatement function = extFile.firstChildOfClass(FunctionStatement.class);
+    FuncallExpression funcall = buildFile.firstChildOfClass(FuncallExpression.class);
+
+    assertThat(function).isNotNull();
+    assertThat(funcall.getReferencedElement()).isEqualTo(function);
+  }
+
+  // relative paths in skylark extensions which lie in subdirectories are relative to the parent blaze package directory
+  public void testRelativePathInSubdirectory() {
+    createFile("java/com/google/BUILD");
+    BuildFile referencedFile = createBuildFile(
+      "java/com/google/nonPackageSubdirectory/skylark.bzl",
+      "def function(): return");
+    BuildFile file = createBuildFile(
+      "java/com/google/nonPackageSubdirectory/other.bzl",
+      "load(" +
+      "    ':nonPackageSubdirectory/skylark.bzl',",
+      "    'function',",
+      ")",
+      "function()"
+    );
+
+    FunctionStatement function = referencedFile.firstChildOfClass(FunctionStatement.class);
+    FuncallExpression funcall = file.firstChildOfClass(FuncallExpression.class);
+
+    assertThat(function).isNotNull();
+    assertThat(funcall.getReferencedElement()).isEqualTo(function);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LocalReferenceTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LocalReferenceTest.java
new file mode 100644
index 0000000..b50325f
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/LocalReferenceTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.*;
+import com.intellij.psi.PsiElement;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that local references (to TargetExpressions within a given file) are correctly resolved.
+ */
+public class LocalReferenceTest extends BuildFileIntegrationTestCase {
+
+  public void testCreatesReference() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "a = 1",
+      "c = a");
+
+    AssignmentStatement[] stmts = file.childrenOfClass(AssignmentStatement.class);
+    assertThat(stmts).hasLength(2);
+    assertThat(stmts[1].getAssignedValue()).isInstanceOf(ReferenceExpression.class);
+
+    ReferenceExpression ref = (ReferenceExpression) stmts[1].getAssignedValue();
+    assertThat(ref.getReference()).isInstanceOf(LocalReference.class);
+  }
+
+  public void testReferenceResolves() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "a = 1",
+      "c = a");
+
+    AssignmentStatement[] stmts = file.childrenOfClass(AssignmentStatement.class);
+    ReferenceExpression ref = (ReferenceExpression) stmts[1].getAssignedValue();
+
+    PsiElement referencedElement = ref.getReferencedElement();
+    assertThat(referencedElement).isEqualTo(stmts[0].getLeftHandSideExpression());
+  }
+
+  public void testTargetInOuterScope() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "a = 1",
+      "function(c = a)");
+
+    TargetExpression target = file.findChildByClass(TargetExpression.class);
+    FuncallExpression funcall = file.findChildByClass(FuncallExpression.class);
+    ReferenceExpression ref = funcall.firstChildOfClass(ReferenceExpression.class);
+    assertThat(ref.getReferencedElement()).isEqualTo(target);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java
new file mode 100644
index 0000000..eed179a
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/references/PackageReferenceTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.references;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.Argument;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.FuncallExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.psi.PsiReference;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests that package references in string literals are correctly resolved.
+ */
+public class PackageReferenceTest extends BuildFileIntegrationTestCase {
+
+  public void testDirectReferenceResolves() {
+    BuildFile buildFile1 = createBuildFile(
+      "java/com/google/tools/BUILD",
+      "# contents");
+
+    BuildFile buildFile2 = createBuildFile(
+      "java/com/google/other/BUILD",
+      "package_group(name = \"grp\", packages = [\"//java/com/google/tools\"])");
+
+    Argument.Keyword packagesArg = buildFile2.firstChildOfClass(FuncallExpression.class).getArgList().getKeywordArgument("packages");
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(packagesArg, StringLiteral.class);
+    assertThat(string.getReferencedElement()).isEqualTo(buildFile1);
+  }
+
+  public void testLabelFragmentResolves() {
+    BuildFile buildFile1 = createBuildFile(
+      "java/com/google/tools/BUILD",
+      "java_library(name = \"lib\")");
+
+    BuildFile buildFile2 = createBuildFile(
+      "java/com/google/other/BUILD",
+      "java_library(name = \"lib2\", exports = [\"//java/com/google/tools:lib\"])");
+
+    FuncallExpression libTarget = buildFile1.firstChildOfClass(FuncallExpression.class);
+    assertThat(libTarget).isNotNull();
+
+    Argument.Keyword packagesArg = buildFile2.firstChildOfClass(FuncallExpression.class).getArgList().getKeywordArgument("exports");
+    StringLiteral string = PsiUtils.findFirstChildOfClassRecursive(packagesArg, StringLiteral.class);
+
+    PsiReference[] references = string.getReferences();
+    assertThat(references).hasLength(2);
+    assertThat(references[0].resolve()).isEqualTo(libTarget);
+    assertThat(references[1].resolve()).isEqualTo(buildFile1);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageTest.java
new file mode 100644
index 0000000..c7e9240
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/BlazePackageTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.search;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.psi.PsiFile;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for BlazePackage
+ */
+public class BlazePackageTest extends BuildFileIntegrationTestCase {
+
+  public void testFindPackage() {
+    BuildFile packageFile = createBuildFile("java/com/google/BUILD");
+    PsiFile subDirFile = createPsiFile("java/com/google/tools/test.txt");
+    BlazePackage blazePackage = BlazePackage.getContainingPackage(subDirFile);
+    assertThat(blazePackage).isNotNull();
+    assertThat(blazePackage.buildFile).isEqualTo(packageFile);
+  }
+
+  public void testScopeDoesntCrossPackageBoundary() {
+    BuildFile pkg = createBuildFile("java/com/google/BUILD");
+    BuildFile subpkg = createBuildFile("java/com/google/other/BUILD");
+
+    BlazePackage blazePackage = BlazePackage.getContainingPackage(pkg);
+    assertThat(blazePackage.buildFile).isEqualTo(pkg);
+    assertFalse(blazePackage.getSearchScope(false).contains(subpkg.getVirtualFile()));
+  }
+
+  public void testScopeIncludesSubdirectoriesWhichAreNotBlazePackages() {
+    BuildFile pkg = createBuildFile("java/com/google/BUILD");
+    BuildFile subpkg = createBuildFile("java/com/google/foo/bar/BUILD");
+    PsiFile subDirFile = createPsiFile("java/com/google/foo/test.txt");
+
+    BlazePackage blazePackage = BlazePackage.getContainingPackage(subDirFile);
+    assertThat(blazePackage.buildFile).isEqualTo(pkg);
+    assertTrue(blazePackage.getSearchScope(false).contains(subDirFile.getVirtualFile()));
+  }
+
+  public void testScopeLimitedToBlazeFiles() {
+    BuildFile pkg = createBuildFile("java/com/google/BUILD");
+    BuildFile subpkg = createBuildFile("java/com/google/foo/bar/BUILD");
+    PsiFile subDirFile = createPsiFile("java/com/google/foo/test.txt");
+
+    BlazePackage blazePackage = BlazePackage.getContainingPackage(subDirFile);
+    assertThat(blazePackage.buildFile).isEqualTo(pkg);
+    assertFalse(blazePackage.getSearchScope(true).contains(subDirFile.getVirtualFile()));
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/GlobalWordIndexTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/GlobalWordIndexTest.java
new file mode 100644
index 0000000..83350b1
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/search/GlobalWordIndexTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.search;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.impl.cache.CacheManager;
+import com.intellij.psi.search.GlobalSearchScope;
+import com.intellij.psi.search.UsageSearchContext;
+import org.intellij.lang.annotations.MagicConstant;
+
+import java.util.Arrays;
+
+/**
+ * Test the WordScanner indexes keywords in the way we expect.<br>
+ * This is vital for navigation, refactoring, highlighting etc.
+ */
+public class GlobalWordIndexTest extends BuildFileIntegrationTestCase {
+
+  public void testWordsInComments() {
+    VirtualFile file = createFile("java/com/google/BUILD",
+                                  "# words in comments");
+    assertContainsWords(file, UsageSearchContext.IN_COMMENTS, "words", "in", "comments");
+  }
+
+  public void testWordsInStrings() {
+    VirtualFile file = createFile("java/com/google/BUILD",
+                                  "name = \"long name with spaces\",",
+                                  "src = [\"name_without_spaces\"]");
+    assertContainsWords(file, UsageSearchContext.IN_STRINGS, "long", "name", "with", "spaces", "name_without_spaces");
+  }
+
+  public void testWordsInCode() {
+    VirtualFile file = createFile("java/com/google/BUILD",
+                                  "java_library(",
+                                  "name = \"long name with spaces\",",
+                                  "src = [\"name_without_spaces\"]",
+                                  ")");
+    assertContainsWords(file, UsageSearchContext.IN_CODE, "java_library", "name", "src");
+  }
+
+  private void assertContainsWords(
+    VirtualFile file,
+    @MagicConstant(flagsFromClass = UsageSearchContext.class) short occurenceMask,
+    String... words) {
+
+    for (String word : words) {
+      VirtualFile[] files = CacheManager.SERVICE.getInstance(getProject()).getVirtualFilesWithWord(
+        word,
+        occurenceMask,
+        GlobalSearchScope.fileScope(getProject(), file),
+        true);
+      if (!Arrays.asList(files).contains(file)) {
+        fail(String.format("Word '%s' not found in file '%s'", word, file));
+      }
+    }
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/GlobValidationTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/GlobValidationTest.java
new file mode 100644
index 0000000..108bfbc
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/buildfile/validation/GlobValidationTest.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.buildfile.validation;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.GlobExpression;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.codeInsight.daemon.impl.AnnotationHolderImpl;
+import com.intellij.lang.annotation.Annotation;
+import com.intellij.lang.annotation.AnnotationHolder;
+import com.intellij.lang.annotation.AnnotationSession;
+import com.intellij.psi.PsiFile;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests glob validation.
+ */
+public class GlobValidationTest extends BuildFileIntegrationTestCase {
+
+  public void testNormalGlob() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'])");
+
+    assertNoErrors(file);
+  }
+
+  public void testNamedIncludeArgument() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(include = ['**/*.java'])");
+
+    assertNoErrors(file);
+  }
+
+  public void testAllArguments() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'], exclude = ['test/*.java'], exclude_directories = 0)");
+
+    assertNoErrors(file);
+  }
+
+  public void testEmptyExcludeList() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'], exclude = [])");
+
+    assertNoErrors(file);
+  }
+
+  public void testNoIncludesError() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(exclude = ['BUILD'])");
+
+    assertHasError(file, "Glob expression must contain at least one included string");
+  }
+
+  public void testSingletonExcludeArgumentError() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'], exclude = 'BUILD')");
+
+    assertHasError(file, "Glob parameter 'exclude' must be a list of strings");
+  }
+
+  public void testSingletonIncludeArgumentError() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(include = '**/*.java')");
+
+    assertHasError(file, "Glob parameter 'include' must be a list of strings");
+  }
+
+  public void testInvalidExcludeDirectoriesValue() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'], exclude = ['test/*.java'], exclude_directories = true)");
+
+    assertHasError(file, "exclude_directories parameter to glob must be 0 or 1");
+  }
+
+  public void testUnrecognizedArgumentError() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(['**/*.java'], exclude = ['test/*.java'], extra = 1)");
+
+    assertHasError(file, "Unrecognized glob argument");
+  }
+
+  public void testInvalidListArgumentValue() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(include = foo)");
+
+    assertHasError(file, "Glob parameter 'include' must be a list of strings");
+  }
+
+  public void testLocalVariableReference() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "foo = ['*.java']",
+      "glob(include = foo)");
+
+    assertNoErrors(file);
+  }
+
+  public void testLoadedVariableReference() {
+    BuildFile ext = createBuildFile(
+      "java/com/foo/vars.bzl",
+      "LIST_VAR = ['*']");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "load('//java/com/foo:vars.bzl', 'LIST_VAR')",
+      "glob(include = LIST_VAR)");
+
+    assertNoErrors(file);
+  }
+
+  public void testInvalidLoadedVariableReference() {
+    BuildFile ext = createBuildFile(
+      "java/com/foo/vars.bzl",
+      "LIST_VAR = ['*']",
+      "def function()");
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "load('//java/com/foo:vars.bzl', 'LIST_VAR', 'function')",
+      "glob(include = function)");
+
+    assertHasError(file, "Glob parameter 'include' must be a list of strings");
+  }
+
+  public void testUnresolvedReferenceExpression() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(include = ref)");
+
+    assertHasError(file, "Glob parameter 'include' must be a list of strings");
+  }
+
+  public void testPossibleListExpressionFuncallExpression() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(include = fn.list)");
+
+    assertNoErrors(file);
+  }
+
+  public void testPossibleListExpressionParameter() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "def function(param1, param2):",
+      "    glob(include = param1)");
+
+    assertNoErrors(file);
+  }
+
+  public void testNestedGlobs() {
+    // blaze accepts nested globs
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(glob(['*.java']))");
+
+    assertNoErrors(file);
+  }
+
+  public void testKnownInvalidResolvedListExpression() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "bool_literal = True",
+      "glob(bool_literal)");
+
+    assertHasError(file, "Glob parameter 'include' must be a list of strings");
+  }
+
+  public void testKnownInvalidResolvedString() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "bool_literal = True",
+      "glob([bool_literal])");
+
+    assertHasError(file, "Glob parameter 'include' must be a list of strings");
+  }
+
+  public void testPossibleStringLiteralIfStatement() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "glob(include = ['*.java', if test : a else b])");
+
+    // we don't know what the IfStatement evaluates to
+    assertNoErrors(file);
+  }
+
+  public void testPossibleStringLiteralParameter() {
+    BuildFile file = createBuildFile(
+      "java/com/google/BUILD",
+      "def function(param1, param2):",
+      "    glob(include = [param1])");
+
+    assertNoErrors(file);
+  }
+
+  private void assertNoErrors(BuildFile file) {
+    assertThat(validateFile(file)).isEmpty();
+  }
+
+  private void assertHasError(BuildFile file, String error) {
+    assertHasError(validateFile(file), error);
+  }
+
+  private void assertHasError(List<Annotation> annotations, String error) {
+    List<String> messages = annotations.stream()
+      .map(Annotation::getMessage)
+      .collect(Collectors.toList());
+
+    assertThat(messages).contains(error);
+  }
+
+  private List<Annotation> validateFile(BuildFile file) {
+    GlobErrorAnnotator annotator = createAnnotator(file);
+    for (GlobExpression glob : PsiUtils.findAllChildrenOfClassRecursive(file, GlobExpression.class)) {
+      annotator.visitGlobExpression(glob);
+    }
+    return annotationHolder;
+  }
+
+  private GlobErrorAnnotator createAnnotator(PsiFile file) {
+    annotationHolder = new AnnotationHolderImpl(new AnnotationSession(file));
+    return new GlobErrorAnnotator() {
+      @Override
+      protected AnnotationHolder getHolder() {
+        return annotationHolder;
+      }
+    };
+  }
+
+  private AnnotationHolderImpl annotationHolder = null;
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java
new file mode 100644
index 0000000..a9b4b79
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewCompletionTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.projectview.section.sections.Sections;
+import com.intellij.codeInsight.lookup.Lookup;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.psi.PsiFile;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests auto-complete in project view files
+ */
+public class ProjectViewCompletionTest extends ProjectViewIntegrationTestCase {
+
+  private PsiFile setInput(String... fileContents) {
+    return testFixture.configureByText(".blazeproject", Joiner.on("\n").join(fileContents));
+  }
+
+  private void assertResult(String... resultingFileContents) {
+    String s = testFixture.getFile().getText();
+    testFixture.checkResult(Joiner.on("\n").join(resultingFileContents));
+  }
+
+  public void testSectionTypeKeywords() {
+    setInput(
+      "<caret>");
+    String[] keywords = getCompletionItemsAsStrings();
+
+    assertThat(keywords).asList().containsAllIn(
+      Sections.getUndeprecatedParsers().stream().map(SectionParser::getName).collect(Collectors.toList()));
+  }
+
+  public void testColonAndNewLineAndIndentInsertedAfterListSection() {
+    setInput(
+      "direc<caret>");
+    assertThat(completeIfUnique()).isTrue();
+    assertResult(
+      "directories:",
+      "  <caret>");
+  }
+
+  public void testWhitespaceDividerInsertedAfterScalarSection() {
+    setInput(
+      "impo<caret>");
+
+    LookupElement[] completionItems = testFixture.completeBasic();
+    assertThat(completionItems[0].getLookupString()).isEqualTo("import");
+
+    testFixture.getLookup().setCurrentItem(completionItems[0]);
+    testFixture.finishLookup(Lookup.NORMAL_SELECT_CHAR);
+
+    assertResult(
+      "import <caret>");
+  }
+
+  public void testColonDividerAndSpaceInsertedAfterScalarSection() {
+    setInput(
+      "works<caret>");
+    assertThat(completeIfUnique()).isTrue();
+    assertResult(
+      "workspace_type: <caret>");
+  }
+
+  public void testNoKeywordCompletionInListItem() {
+    setInput(
+      "directories:",
+      "  <caret>");
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    if (completionItems == null) {
+      fail("Spurious completion. New file contents: " + testFixture.getFile().getText());
+    }
+    assertThat(completionItems).isEmpty();
+  }
+
+  public void testNoKeywordCompletionAfterKeyword() {
+    setInput(
+      "import <caret>");
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    if (completionItems == null) {
+      fail("Spurious completion. New file contents: " + testFixture.getFile().getText());
+    }
+    assertThat(completionItems).isEmpty();
+  }
+
+  public void testWorkspaceTypeCompletion() {
+    setInput(
+      "workspace_type: <caret>");
+
+    String[] types = getCompletionItemsAsStrings();
+
+    assertThat(types).asList().containsAllIn(
+      Arrays.stream(WorkspaceType.values()).map(WorkspaceType::getName).collect(Collectors.toList()));
+  }
+
+  public void testAdditionalLanguagesCompletion() {
+    setInput(
+      "additional_languages:",
+      "  <caret>");
+
+    String[] types = getCompletionItemsAsStrings();
+
+    assertThat(types).asList().containsAllIn(
+      Arrays.stream(LanguageClass.values()).map(LanguageClass::getName).collect(Collectors.toList()));
+  }
+
+  public void testUniqueDirectoryCompleted() {
+    setInput(
+      "import <caret>");
+
+    createDirectory("java");
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).isNull();
+    assertResult(
+      "import java<caret>"
+    );
+  }
+
+  public void testUniqueMultiSegmentDirectoryCompleted() {
+    setInput(
+      "import <caret>");
+
+    createDirectory("java/com/google");
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).isNull();
+    assertResult(
+      "import java/com/google<caret>"
+    );
+  }
+
+  public void testNonDirectoriesIgnored() {
+    setInput(
+      "import <caret>");
+
+    createDirectory("java/com/google");
+    createFile("java/IgnoredFile.java");
+
+    String[] completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).isNull();
+    assertResult(
+      "import java/com/google<caret>"
+    );
+  }
+
+  public void testMultipleDirectoryOptions() {
+    createDirectory("foo");
+    createDirectory("bar");
+    createDirectory("other");
+    createDirectory("ostrich/foo");
+    createDirectory("ostrich/fooz");
+
+    setInput(
+      "targets:",
+      "  //o<caret>");
+
+    String[] completionItems = getCompletionItemsAsSuggestionStrings();
+    assertThat(completionItems).asList().containsExactly("other", "ostrich");
+
+    performTypingAction(testFixture.getEditor(), 's');
+
+    completionItems = getCompletionItemsAsStrings();
+    assertThat(completionItems).isNull();
+    assertResult(
+      "targets:",
+      "  //ostrich<caret>");
+  }
+
+  public void testRuleCompletion() {
+    createFile(
+      "BUILD",
+      "java_library(name = 'lib')"
+    );
+
+    setInput(
+      "targets:",
+      "  //:<caret>");
+
+    String[] completionItems = getCompletionItemsAsSuggestionStrings();
+    assertThat(completionItems).isNull();
+    assertResult(
+      "targets:",
+      "  //:lib<caret>");
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.java
new file mode 100644
index 0000000..a282bdb
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewIntegrationTestCase.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.projectview;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+
+/**
+ * Project view file specific integration test base
+ */
+public abstract class ProjectViewIntegrationTestCase extends BlazeIntegrationTestCase {
+
+  @Override
+  protected void doSetup() {
+    mockBlazeProjectDataManager(getMockBlazeProjectData());
+  }
+
+  private BlazeProjectData getMockBlazeProjectData() {
+    BlazeRoots fakeRoots = new BlazeRoots(
+      null,
+      ImmutableList.of(workspaceRoot.directory()),
+      new ExecutionRootPath("out/crosstool/bin"),
+      new ExecutionRootPath("out/crosstool/gen")
+    );
+    return new BlazeProjectData(0,
+                                ImmutableMap.of(),
+                                fakeRoots,
+                                new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+                                new WorkspacePathResolverImpl(workspaceRoot, fakeRoots),
+                                null,
+                                null,
+                                null);
+  }
+
+}
\ No newline at end of file
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewParserTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewParserTest.java
new file mode 100644
index 0000000..b6a081c
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/ProjectViewParserTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.projectview;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.lang.projectview.psi.ProjectViewPsiElement;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiErrorElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.impl.source.tree.LeafElement;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for the project view file parser
+ */
+public class ProjectViewParserTest extends ProjectViewIntegrationTestCase {
+
+  private final List<String> errors = Lists.newArrayList();
+
+  @Override
+  protected void doSetup() {
+    errors.clear();
+    super.doSetup();
+  }
+
+  public void testStandardFile() {
+    assertThat(parse(
+      "directories:",
+      "  java/com/google/work",
+      "  java/com/google/other",
+      "",
+      "targets:",
+      "  //java/com/google/work/...:all",
+      "  //java/com/google/other/...:all"
+    )).isEqualTo(Joiner.on("").join(
+      "list_section(list_item, list_item), ",
+      "list_section(list_item, list_item)"
+    ));
+    assertNoErrors();
+  }
+
+  public void testIncludeScalarSections() {
+    assertThat(parse(
+      "import java/com/google/work/.blazeproject",
+      "",
+      "workspace_type: intellij_plugin",
+      "",
+      "import_target_output:",
+      "  //java/com/google/work:target",
+      "",
+      "excluded_libraries:",
+      "  java/com/google/common/*"
+    )).isEqualTo(Joiner.on("").join(
+      "scalar_section(scalar_item), ",
+      "scalar_section(scalar_item), ",
+      "list_section(list_item), ",
+      "list_section(list_item)"
+    ));
+    assertNoErrors();
+  }
+
+  public void testUnrecognizedKeyword() {
+    parse(
+      "impart java/com/google/work/.blazeproject",
+      "",
+      "workspace_trype: intellij_plugin"
+    );
+
+    assertContainsErrors(
+      "Unrecognized keyword: impart",
+      "Unrecognized keyword: workspace_trype");
+  }
+
+  private String parse(String... lines) {
+    PsiFile file = createPsiFile(".blazeproject", lines);
+    collectErrors(file);
+    return treeToString(file);
+  }
+
+  private String treeToString(PsiElement psi) {
+    StringBuilder builder = new StringBuilder();
+    nodeToString(psi, builder);
+    return builder.toString();
+  }
+
+  private void nodeToString(PsiElement psi, StringBuilder builder) {
+    if (psi.getNode() instanceof LeafElement) {
+      return;
+    }
+    PsiElement[] children = Arrays.stream(psi.getChildren())
+      .filter(t -> t instanceof ProjectViewPsiElement)
+      .toArray(PsiElement[]::new);
+    if (psi instanceof ProjectViewPsiElement) {
+      builder.append(psi.getNode().getElementType());
+      appendChildren(children, builder, true);
+    } else {
+      appendChildren(children, builder, false);
+    }
+  }
+
+  private void appendChildren(PsiElement[] childPsis, StringBuilder builder, boolean bracket) {
+    if (childPsis.length == 0) {
+      return;
+    }
+    if (bracket) {
+      builder.append("(");
+    }
+    nodeToString(childPsis[0], builder);
+    for (int i = 1; i < childPsis.length; i++) {
+      builder.append(", ");
+      nodeToString(childPsis[i], builder);
+    }
+    if (bracket) {
+      builder.append(")");
+    }
+  }
+
+  private void assertNoErrors() {
+    assertThat(errors).isEmpty();
+  }
+
+  private void assertContainsErrors(String... errors) {
+    assertThat(this.errors).containsAllIn(Arrays.asList(errors));
+  }
+
+  private void collectErrors(PsiElement psi) {
+    errors.addAll(PsiUtils.findAllChildrenOfClassRecursive(psi, PsiErrorElement.class)
+                    .stream()
+                    .map(PsiErrorElement::getErrorDescription)
+                    .collect(Collectors.toList()));
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerTest.java
new file mode 100644
index 0000000..f37da9e
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/lang/projectview/lexer/ProjectViewLexerTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.lang.projectview.lexer;
+
+import com.google.common.base.Joiner;
+import com.google.idea.blaze.base.lang.projectview.ProjectViewIntegrationTestCase;
+import com.google.idea.blaze.base.lang.projectview.lexer.ProjectViewLexerBase.Token;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for the project view file lexer
+ */
+public class ProjectViewLexerTest extends ProjectViewIntegrationTestCase {
+
+  public void testStandardCase() {
+    String result = tokenize(
+      "directories:",
+      "  java/com/google/work",
+      "  java/com/google/other",
+      "",
+      "targets:",
+      "  //java/com/google/work/...:all",
+      "  //java/com/google/other/...:all");
+
+    assertThat(result).isEqualTo(Joiner.on(" ").join(
+      "list_keyword :",
+      "indent identifier",
+      "indent identifier",
+      "list_keyword :",
+      "indent identifier : identifier",
+      "indent identifier : identifier"));
+  }
+
+  public void testIncludeScalarSections() {
+    String result = tokenize(
+      "import java/com/google/work/.blazeproject",
+      "",
+      "workspace_type: intellij_plugin",
+      "",
+      "import_target_output:",
+      "  //java/com/google/work:target",
+      "",
+      "excluded_libraries:",
+      "  java/com/google/common/*");
+
+    assertThat(result).isEqualTo(Joiner.on(" ").join(
+      "scalar_keyword identifier",
+      "scalar_keyword : identifier",
+      "list_keyword :",
+      "indent identifier : identifier",
+      "list_keyword :",
+      "indent identifier"));
+  }
+
+  public void testUnrecognizedKeyword() {
+    String result = tokenize(
+      "impart java/com/google/work/.blazeproject",
+      "",
+      "workspace_trype: intellij_plugin");
+
+    assertThat(result).isEqualTo(Joiner.on(" ").join(
+      "identifier identifier",
+      "identifier : identifier"));
+  }
+
+  private static String tokenize(String... lines) {
+    return names(tokens(Joiner.on("\n").join(lines)));
+  }
+
+  private static Token[] tokens(String input) {
+    Token[] tokens = new ProjectViewLexerBase(input).getTokens().toArray(new Token[0]);
+    assertNoCharactersMissing(input.length(), tokens);
+    return tokens;
+  }
+
+  /**
+   * Both the syntax highlighter and the parser require every character be accounted for by
+   * a lexical element.
+   */
+  private static void assertNoCharactersMissing(int totalLength, Token[] tokens) {
+    if (tokens.length != 0 && tokens[tokens.length - 1].right != totalLength) {
+      throw new AssertionError(String.format(
+        "Last tokenized character '%s' doesn't match document length '%s'",
+        tokens[tokens.length - 1].right, totalLength));
+    }
+    int start = 0;
+    for (int i = 0; i < tokens.length; i++) {
+      Token token = tokens[i];
+      if (token.left != start) {
+        throw new AssertionError("Gap/inconsistency at: " + start);
+      }
+      start = token.right;
+    }
+  }
+
+  /**
+   * Returns a string containing the names of the tokens.
+   */
+  private static String names(Token[] tokens) {
+    StringBuilder buf = new StringBuilder();
+    for (Token token : tokens) {
+      if (isIgnored(token.type)) {
+        continue;
+      }
+      if (buf.length() > 0) {
+        buf.append(' ');
+      }
+      buf.append(token.type);
+    }
+    return buf.toString();
+  }
+
+  private static boolean isIgnored(ProjectViewTokenType kind) {
+    return kind == ProjectViewTokenType.WHITESPACE || kind == ProjectViewTokenType.NEWLINE;
+  }
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/sync/BlazeSyncTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/sync/BlazeSyncTest.java
new file mode 100644
index 0000000..04fd4ac
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/sync/BlazeSyncTest.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.base.sync;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.sync.actions.IncrementalSyncProjectAction;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for the blaze sync process {@link BlazeSyncTask}
+ */
+public class BlazeSyncTest extends BlazeSyncIntegrationTestCase {
+
+  public void testSimpleSync() throws Exception {
+    setProjectView(
+      "directories:",
+      "  java/com/google",
+      "targets:",
+      "  //java/com/google:lib",
+      "workspace_type: java"
+    );
+
+    createFile(
+      "java/com/google/Source.java",
+      "package com.google;",
+      "public class Source {}"
+    );
+
+    createFile(
+      "java/com/google/Other.java",
+      "package com.google;",
+      "public class Other {}"
+    );
+
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = RuleMapBuilder.builder()
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                 .setLabel("//java/com/google:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("java/com/google/Source.java"))
+                 .addSource(sourceRoot("java/com/google/Other.java")))
+      .build();
+
+    setRuleMap(ruleMap);
+
+    runBlazeSync(IncrementalSyncProjectAction.manualSyncParams);
+
+    assertNoErrors();
+
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    assertThat(blazeProjectData).isNotNull();
+    assertThat(blazeProjectData.ruleMap).isEqualTo(ruleMap);
+    assertThat(blazeProjectData.workspaceLanguageSettings.getWorkspaceType())
+      .isEqualTo(WorkspaceType.JAVA);
+  }
+
+}
diff --git a/blaze-base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java
new file mode 100644
index 0000000..8fe8f63
--- /dev/null
+++ b/blaze-base/tests/integrationtests/com/google/idea/blaze/base/sync/ImportRootsTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.ImmutableList;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+
+import java.util.stream.Collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for ImportRoots
+ */
+public class ImportRootsTest extends BlazeIntegrationTestCase {
+
+  public void testBazelArtifactDirectoriesExcluded() {
+    ImportRoots importRoots = ImportRoots.builder(workspaceRoot, BuildSystem.Bazel)
+      .add(new DirectoryEntry(new WorkspacePath(""), true))
+      .build();
+
+    ImmutableList<String> artifactDirs = BuildSystemProvider.getBuildSystemProvider(BuildSystem.Bazel)
+      .buildArtifactDirectories(workspaceRoot);
+
+    assertThat(importRoots.rootDirectories()).containsExactly(new WorkspacePath(""));
+    assertThat(
+      importRoots.excludeDirectories()
+        .stream()
+        .map(WorkspacePath::relativePath)
+        .collect(Collectors.toList())
+    ).containsExactlyElementsIn(artifactDirs);
+
+    assertThat(artifactDirs).contains("bazel-" + workspaceRoot.directory().getName());
+  }
+
+  public void testNoAddedExclusionsWithoutWorkspaceRootInclusion() {
+    ImportRoots importRoots = ImportRoots.builder(workspaceRoot, BuildSystem.Bazel)
+      .add(new DirectoryEntry(new WorkspacePath("foo/bar"), true))
+      .build();
+
+    assertThat(importRoots.rootDirectories()).containsExactly(new WorkspacePath("foo/bar"));
+    assertThat(importRoots.excludeDirectories()).isEmpty();
+  }
+
+  public void testNoAddedExclusionsForBlaze() {
+    ImportRoots importRoots = ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
+      .add(new DirectoryEntry(new WorkspacePath(""), true))
+      .build();
+
+    assertThat(importRoots.rootDirectories()).containsExactly(new WorkspacePath(""));
+    assertThat(importRoots.excludeDirectories()).isEmpty();
+  }
+
+  // if the workspace root is an included directory, all rules should be imported as sources.
+  public void testAllLabelsIncludedUnderWorkspaceRoot() {
+    ImportRoots importRoots = ImportRoots.builder(workspaceRoot, BuildSystem.Blaze)
+      .add(new DirectoryEntry(new WorkspacePath(""), true))
+      .build();
+
+    assertThat(importRoots.importAsSource(new Label("//:target"))).isTrue();
+    assertThat(importRoots.importAsSource(new Label("//foo/bar:target"))).isTrue();
+  }
+
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/BlazeIconsTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/BlazeIconsTest.java
new file mode 100644
index 0000000..85fbc34
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/BlazeIconsTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base;
+
+import static org.junit.Assert.assertNotNull;
+
+import org.junit.Test;
+
+import javax.swing.Icon;
+
+import icons.BlazeIcons;
+
+/**
+ * Tests for BlazeIcons
+ */
+public class BlazeIconsTest {
+
+  @Test
+  public void testIcon() {
+    Icon icon = BlazeIcons.Blaze;
+
+    assertNotNull(icon);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandNameTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandNameTest.java
new file mode 100644
index 0000000..7a55d93
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandNameTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for {@link BlazeCommandName}.
+ */
+@RunWith(JUnit4.class)
+public class BlazeCommandNameTest {
+  @Test
+  public void emptyNameShouldThrow() {
+    try {
+      BlazeCommandName.fromString("");
+      fail("Empty commands should not be allowed.");
+    }
+    catch (IllegalArgumentException expected) {
+    }
+  }
+
+  @Test
+  public void hardcodedNamesShouldBeKnown() {
+    assertThat(BlazeCommandName.knownCommands()).contains(BlazeCommandName.MOBILE_INSTALL);
+  }
+
+  @Test
+  public void userCommandNamesShouldBecomeKnown() {
+    Collection<String> knownCommandStrings =
+      Collections2.transform(BlazeCommandName.knownCommands(),
+                             new Function<BlazeCommandName, String>() {
+                               @Nullable
+                               @Override
+                               public String apply(BlazeCommandName input) {
+                                 return input.toString();
+                               }
+                             });
+    assertThat(knownCommandStrings).doesNotContain("user-command");
+    BlazeCommandName userCommand = BlazeCommandName.fromString("user-command");
+    assertThat(BlazeCommandName.knownCommands()).contains(userCommand);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java
new file mode 100644
index 0000000..1782ebc
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/command/BlazeCommandTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.command;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for {@link BlazeCommand}.
+ */
+@RunWith(JUnit4.class)
+public class BlazeCommandTest extends BlazeTestCase {
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    ExperimentService experimentService = new MockExperimentService();
+    applicationServices.register(ExperimentService.class, experimentService);
+    applicationServices.register(BlazeUserSettings.class, new BlazeUserSettings());
+  }
+
+  @Test
+  public void addedFlagsShouldGoAtStart() {
+    List<String> flagsCommand = BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+      .addTargets(new Label("//a:b"))
+      .addBlazeFlags("--flag1", "--flag2")
+      .addExeFlags("--exeFlag1", "--exeFlag2")
+      .build()
+      .toList();
+    // First three strings are always 'blaze run --tool_tag=ijwb:IDEA:ultimate'
+    assertThat(flagsCommand.subList(3, 5))
+      .isEqualTo(ImmutableList.of("--flag1", "--flag2"));
+  }
+
+  @Test
+  public void targetsShouldGoAfterBlazeFlagsAndDoubleHyphen() {
+    List<String> command = BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+      .addTargets(new Label("//a:b"), new Label("//c:d"))
+      .addBlazeFlags("--flag1", "--flag2")
+      .addExeFlags("--exeFlag1", "--exeFlag2")
+      .build()
+      .toList();
+    // First six strings should be 'blaze run --tool_tag=ijwb:IDEA:ultimate --flag1 --flag2 --'
+    assertThat(command.indexOf("--")).isEqualTo(5);
+    assertThat(Collections.indexOfSubList(command, ImmutableList.of("//a:b", "//c:d"))).isEqualTo(6);
+  }
+
+  @Test
+  public void exeFlagsShouldGoLast() {
+    List<String> command = BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+      .addTargets(new Label("//a:b"), new Label("//c:d"))
+      .addBlazeFlags("--flag1", "--flag2")
+      .addExeFlags("--exeFlag1", "--exeFlag2")
+      .build()
+      .toList();
+    List<String> finalTwoFlags = command.subList(command.size() - 2, command.size());
+    assertThat(finalTwoFlags).containsExactly("--exeFlag1", "--exeFlag2");
+  }
+
+  @Test
+  public void maintainUserOrderingOfTargets() {
+    List<String> command = BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.RUN)
+      .addTargets(new Label("//a:b"), TargetExpression.fromString("-//e:f"), new Label("//c:d"))
+      .addBlazeFlags("--flag1", "--flag2")
+      .addExeFlags("--exeFlag1", "--exeFlag2")
+      .build()
+      .toList();
+
+    ImmutableList<Object> expected = ImmutableList.builder()
+      .add("/usr/bin/blaze")
+      .add("run")
+      .add(BlazeFlags.getToolTagFlag())
+      .add("--flag1")
+      .add("--flag2")
+      .add("--")
+      .add("//a:b")
+      .add("-//e:f")
+      .add("//c:d")
+      .add("--exeFlag1")
+      .add("--exeFlag2")
+      .build();
+    assertThat(command).isEqualTo(expected);
+  }
+
+  @Test
+  public void binaryAndCommandShouldComeFirst() {
+    List<String> command = BlazeCommand.builder(BuildSystem.Blaze, BlazeCommandName.BUILD)
+      .addBlazeFlags("--flag")
+      .addExeFlags("--exeFlag")
+      .build()
+      .toList();
+    assertThat(command.subList(0, 2)).isEqualTo(ImmutableList.of("/usr/bin/blaze", "build"));
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/experiments/ExperimentServiceImplTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/experiments/ExperimentServiceImplTest.java
new file mode 100644
index 0000000..15255e4
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/experiments/ExperimentServiceImplTest.java
@@ -0,0 +1,120 @@
+/*
+ * 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.experiments;
+
+import com.google.common.collect.ImmutableMap;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for {@link ExperimentServiceImpl}.
+ */
+public class ExperimentServiceImplTest {
+
+  @Test
+  public void testBooleanPropertyTrue() {
+    ExperimentService experimentService =
+        new ExperimentServiceImpl(new MapExperimentLoader("test.property", "1"));
+    assertThat(experimentService.getExperiment("test.property", false)).isTrue();
+  }
+
+  @Test
+  public void testBooleanPropertyFalse() {
+    ExperimentService experimentService =
+        new ExperimentServiceImpl(new MapExperimentLoader("test.property", "0"));
+    assertThat(experimentService.getExperiment("test.property", true)).isFalse();
+  }
+
+  @Test
+  public void testBooleanPropertyReturnsDefaultWhenMissing() {
+    ExperimentService experimentService = new ExperimentServiceImpl(new MapExperimentLoader());
+    assertThat(experimentService.getExperiment("test.notthere", true)).isTrue();
+  }
+
+  @Test
+  public void testStringProperty() {
+    ExperimentService experimentService =
+        new ExperimentServiceImpl(new MapExperimentLoader("test.property", "hi"));
+    assertThat(experimentService.getExperimentString("test.property", null)).isEqualTo("hi");
+  }
+
+  @Test
+  public void testStringPropertyReturnsDefaultWhenMissing() {
+    ExperimentService experimentService = new ExperimentServiceImpl(new MapExperimentLoader());
+    assertThat(experimentService.getExperimentString("test.property", "bye")).isEqualTo("bye");
+  }
+
+  @Test
+  public void testFirstLoaderOverridesSecond() {
+    ExperimentService experimentService =
+        new ExperimentServiceImpl(
+            new MapExperimentLoader("test.property", "1"),
+            new MapExperimentLoader("test.property", "0"));
+    assertThat(experimentService.getExperiment("test.property", false)).isTrue();
+  }
+
+  @Test
+  public void testOnlyInSecondLoader() {
+    ExperimentService experimentService =
+        new ExperimentServiceImpl(
+            new MapExperimentLoader(), new MapExperimentLoader("test.property", "1"));
+    assertThat(experimentService.getExperiment("test.property", false)).isTrue();
+  }
+
+  @Test
+  public void testIntProperty() {
+    ExperimentService experimentService =
+        new ExperimentServiceImpl(new MapExperimentLoader("test.property", "10"));
+    assertThat(experimentService.getExperimentInt("test.property", 0)).isEqualTo(10);
+  }
+
+  @Test
+  public void testIntPropertyDefaultValue() {
+    ExperimentService experimentService = new ExperimentServiceImpl(new MapExperimentLoader());
+    assertThat(experimentService.getExperimentInt("test.property", 100)).isEqualTo(100);
+  }
+
+  @Test
+  public void testIntPropertyThatDoesntParseReturnsDefaultValue() {
+    ExperimentService experimentService =
+        new ExperimentServiceImpl(new MapExperimentLoader("test.property", "hello"));
+    assertThat(experimentService.getExperimentInt("test.property", 111)).isEqualTo(111);
+  }
+
+  private static class MapExperimentLoader implements ExperimentLoader {
+
+    private final Map<String, String> map;
+
+    private MapExperimentLoader(String... keysAndValues) {
+      checkState(keysAndValues.length % 2 == 0);
+      ImmutableMap.Builder<String, String> mapBuilder = ImmutableMap.builder();
+      for (int i = 0; i < keysAndValues.length; i += 2) {
+        mapBuilder.put(keysAndValues[i], keysAndValues[i + 1]);
+      }
+      map = mapBuilder.build();
+    }
+
+    @Override
+    public Map<String, String> getExperiments() {
+      return map;
+    }
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/experiments/SystemPropertyExperimentLoaderTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/experiments/SystemPropertyExperimentLoaderTest.java
new file mode 100644
index 0000000..61eb30d
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/experiments/SystemPropertyExperimentLoaderTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.experiments;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class SystemPropertyExperimentLoaderTest {
+
+  private static final String EXPERIMENT = "test.foo";
+  private static final String PROPERTY = "blaze.experiment.test.foo";
+  private static final String VALUE = "true";
+
+  @Before
+  public void setUp() {
+    System.setProperty(PROPERTY, VALUE);
+  }
+
+  @After
+  public void tearDown() {
+    System.clearProperty(PROPERTY);
+  }
+
+  @Test
+  public void testGetExperiment() {
+    ExperimentLoader loader = new SystemPropertyExperimentLoader();
+    assertThat(loader.getExperiments().get(EXPERIMENT)).isEqualTo(VALUE);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/io/MockWorkspaceScanner.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/io/MockWorkspaceScanner.java
new file mode 100644
index 0000000..9229bfb
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/io/MockWorkspaceScanner.java
@@ -0,0 +1,75 @@
+/*
+ * 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.io;
+
+import com.google.common.collect.Sets;
+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.ProjectViewSet;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Set;
+
+/**
+ * Mocks the file system.
+ */
+public final class MockWorkspaceScanner implements WorkspaceScanner {
+
+  Set<WorkspacePath> files = Sets.newHashSet();
+  Set<WorkspacePath> directories = Sets.newHashSet();
+
+  public MockWorkspaceScanner addFile(@NotNull WorkspacePath file) {
+    files.add(file);
+    return this;
+  }
+
+  public MockWorkspaceScanner addDirectory(@NotNull WorkspacePath file) {
+    addFile(file);
+    directories.add(file);
+    return this;
+  }
+
+  public MockWorkspaceScanner addPackage(@NotNull WorkspacePath file) {
+    addFile(new WorkspacePath(file + "/BUILD"));
+    addDirectory(file);
+    return this;
+  }
+
+  public MockWorkspaceScanner addPackages(@NotNull Iterable<WorkspacePath> files) {
+    for (WorkspacePath workspacePath : files) {
+      addPackage(workspacePath);
+    }
+    return this;
+  }
+
+  public MockWorkspaceScanner addImportRoots(@NotNull ImportRoots importRoots) {
+    addPackages(importRoots.rootDirectories());
+    addPackages(importRoots.excludeDirectories());
+    return this;
+  }
+
+  public MockWorkspaceScanner addProjectView(WorkspaceRoot workspaceRoot, ProjectViewSet projectViewSet) {
+    ImportRoots importRoots = ImportRoots.builder(workspaceRoot, BuildSystem.Blaze).add(projectViewSet).build();
+    return addImportRoots(importRoots);
+  }
+
+  @Override
+  public boolean exists(WorkspaceRoot workspaceRoot, WorkspacePath file) {
+    return files.contains(file);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
new file mode 100644
index 0000000..7b256e6
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/issueparser/BlazeIssueParserTest.java
@@ -0,0 +1,270 @@
+/*
+ * 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.issueparser;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectView;
+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.sections.TargetSection;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.File;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link BlazeIssueParser}.
+ */
+public class BlazeIssueParserTest extends BlazeTestCase {
+
+  private ProjectViewManager projectViewManager;
+  private WorkspaceRoot workspaceRoot;
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+
+    projectViewManager = mock(ProjectViewManager.class);
+    projectServices.register(ProjectViewManager.class, projectViewManager);
+
+    workspaceRoot = new WorkspaceRoot(new File("/root"));
+  }
+
+  @Test
+  public void testParseTargetError() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "ERROR: invalid target format '//javatests/com/google/devtools/aswb/testapps/aswbtestlib/...:alls': invalid package name 'javatests/com/google/devtools/aswb/testapps/aswbtestlib/...': package name component contains only '.' characters."
+    );
+    assertNotNull(issue);
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testParseCompileError() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "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");
+    assertThat(issue.getLine()).isEqualTo(17);
+    assertThat(issue.getMessage()).isEqualTo("non-static variable this cannot be referenced from a static context");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testParseCompileErrorWithColumn() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    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.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);
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "ERROR: /path/to/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.getLine()).isEqualTo(42);
+    assertThat(issue.getMessage()).isEqualTo("Target '//java/package_path:helloroot_visibility' failed");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testParseLinelessBuildError() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "ERROR: /path/to/root/java/package_path/BUILD:char offsets 1222--1229: name 'grubber' is not defined"
+    );
+    assertNotNull(issue);
+    assertThat(issue.getFile().getPath()).isEqualTo(
+      "/path/to/root/java/package_path/BUILD");
+    assertThat(issue.getMessage()).isEqualTo("name 'grubber' is not defined");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testLabelProjectViewParser() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(new File(".blazeproject"), ProjectView.builder()
+             .put(ListSection.builder(TargetSection.KEY)
+                    .add(TargetExpression.fromString("//package/path:hello4")))
+             .build())
+      .build();
+    when(projectViewManager.getProjectViewSet()).thenReturn(projectViewSet);
+
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "no such target '//package/path:hello4': target 'hello4' not declared in package 'package/path' defined by /path/to/root/package/path/BUILD"
+    );
+    assertNotNull(issue);
+    assertThat(issue.getFile().getPath()).isEqualTo(".blazeproject");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testPackageProjectViewParser() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(new File(".blazeproject"), ProjectView.builder()
+        .put(ListSection.builder(TargetSection.KEY)
+               .add(TargetExpression.fromString("//package/path:hello4")))
+        .build())
+      .build();
+    when(projectViewManager.getProjectViewSet()).thenReturn(projectViewSet);
+
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "no such package 'package/path': BUILD file not found on package path"
+    );
+    assertNotNull(issue);
+    assertThat(issue.getFile().getPath()).isEqualTo(".blazeproject");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testDeletedBUILDFileButLeftPackageInLocalTargets() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(new File(".blazeproject"), ProjectView.builder()
+        .put(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);
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "Error:com.google.a.b.Exception exception in Bar: no targets found beneath " +
+      "'tests/com/google/a/b/c/d/baz' Thrown during call: ..."
+    );
+    assertNotNull(issue);
+    assertNotNull(issue.getFile());
+    assertThat(issue.getFile().getPath()).isEqualTo(".blazeproject");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+    assertThat(issue.getMessage()).isEqualTo(
+      "no targets found beneath 'tests/com/google/a/b/c/d/baz'"
+    );
+  }
+
+  @Test
+  public void testMultilineTraceback() {
+    String[] lines = new String[]{
+      "ERROR: /home/plumpy/whatever:9:12: Traceback (most recent call last):",
+      "\tFile \"/path/to/root/java/com/google/android/samples/helloroot/BUILD\", line 8",
+      "\t\tpackage_group(name = BAD_FUNCTION(\"hellogoogle...\"), ...\"])",
+      "\tFile \"/path/to/root/java/com/google/android/samples/helloroot/BUILD\", line 9, in package_group",
+      "\t\tBAD_FUNCTION",
+      "name 'BAD_FUNCTION' is not defined."};
+
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    for (int i = 0; i < lines.length - 1; ++i) {
+      IssueOutput issue = blazeIssueParser.parseIssue(lines[i]);
+      assertNull(issue);
+    }
+
+    IssueOutput issue = blazeIssueParser.parseIssue(lines[lines.length - 1]);
+    assertNotNull(issue);
+    assertThat(issue.getFile().getPath()).isEqualTo("/home/plumpy/whatever");
+    assertThat(issue.getMessage().split("\n")).hasLength(lines.length);
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testLineAfterTracebackIsAlsoParsed() {
+    String[] lines = new String[]{
+      "ERROR: /home/plumpy/whatever:9:12: Traceback (most recent call last):",
+      "\tFile \"/path/to/root/java/com/google/android/samples/helloroot/BUILD\", line 8",
+      "\t\tpackage_group(name = BAD_FUNCTION(\"hellogoogle...\"), ...\"])",
+      "\tFile \"/path/to/root/java/com/google/android/samples/helloroot/BUILD\", line 9, in package_group",
+      "\t\tBAD_FUNCTION",
+      "name 'BAD_FUNCTION' is not defined."};
+
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    for (int i = 0; i < lines.length; ++i) {
+      blazeIssueParser.parseIssue(lines[i]);
+    }
+
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "ERROR: /home/plumpy/whatever:char offsets 1222--1229: name 'grubber' is not defined"
+    );
+    assertNotNull(issue);
+    assertThat(issue.getFile().getPath()).isEqualTo("/home/plumpy/whatever");
+    assertThat(issue.getMessage()).isEqualTo("name 'grubber' is not defined");
+    assertThat(issue.getCategory()).isEqualTo(IssueOutput.Category.ERROR);
+  }
+
+  @Test
+  public void testMultipleIssues() {
+    BlazeIssueParser blazeIssueParser = new BlazeIssueParser(project, workspaceRoot);
+    IssueOutput issue = blazeIssueParser.parseIssue(
+      "ERROR: /home/plumpy/whatever:char offsets 1222--1229: name 'grubber' is not defined"
+    );
+    assertNotNull(issue);
+    issue = blazeIssueParser.parseIssue(
+      "ERROR: /home/plumpy/whatever:char offsets 1222--1229: name 'grubber' is not defined"
+    );
+    assertNotNull(issue);
+    issue = blazeIssueParser.parseIssue(
+      "ERROR: /home/plumpy/whatever:char offsets 1222--1229: name 'grubber' is not defined"
+    );
+    assertNotNull(issue);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/metrics/ActionTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/metrics/ActionTest.java
new file mode 100644
index 0000000..6e0d869
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/metrics/ActionTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.metrics;
+
+import com.google.common.collect.Sets;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ActionTest {
+
+  @Test
+  public void ensureAllActionEnumsHaveUniqueNames() {
+    HashSet<String> names = Sets.newHashSet();
+    for (Action action : Action.values()) {
+      String name = action.getName();
+      Assert.assertTrue(name + " is not unique", names.add(name));
+    }
+  }
+
+  @Test
+  public void ensureAllActionEnumNamesAreAlphanumeric() {
+    Pattern pattern = Pattern.compile("[a-zA-Z0-9]*");
+    for (Action action : Action.values()) {
+      String name = action.getName();
+      Matcher matcher = pattern.matcher(name);
+      Assert.assertTrue(name + " is not valid", matcher.matches());
+    }
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/DeepEqualsTesterTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/DeepEqualsTesterTest.java
new file mode 100644
index 0000000..8227ee1
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/DeepEqualsTesterTest.java
@@ -0,0 +1,560 @@
+/*
+ * 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.model.blaze;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.model.blaze.deepequalstester.DeepEqualsTester;
+import com.google.idea.blaze.base.model.blaze.deepequalstester.DeepEqualsTester.TestCorrectnessException;
+import com.google.idea.blaze.base.model.blaze.deepequalstester.DeepEqualsTesterUtil;
+import com.google.idea.blaze.base.model.blaze.deepequalstester.Examples;
+import com.google.idea.blaze.base.model.blaze.deepequalstester.Examples.ExampleNotFoundException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Tests to verify that our equals tester is working correctly
+ */
+public class DeepEqualsTesterTest {
+
+  // The equals method does not work correctly if T is an array
+  private static class Box<T> implements Serializable {
+
+    public T data;
+
+    public Box(T data) {
+      this.data = data;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      Box<?> box = (Box<?>)o;
+      return Objects.equal(data, box.data);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(data);
+    }
+  }
+
+  private static class CorrectEqualsAndHash implements Serializable {
+
+    public String name;
+
+    public CorrectEqualsAndHash(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      CorrectEqualsAndHash foo = (CorrectEqualsAndHash)o;
+      return Objects.equal(name, foo.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(name);
+    }
+  }
+
+  private static class ClassWithCorrectEqualsMember implements Serializable {
+
+    public String myName;
+    public CorrectEqualsAndHash myCorrectEqualsAndHash;
+
+    public ClassWithCorrectEqualsMember(String name, String innerName) {
+      this.myName = name;
+      this.myCorrectEqualsAndHash = new CorrectEqualsAndHash(innerName);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      ClassWithCorrectEqualsMember that = (ClassWithCorrectEqualsMember)o;
+      return Objects.equal(myName, that.myName) &&
+             Objects.equal(myCorrectEqualsAndHash, that.myCorrectEqualsAndHash);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(myName, myCorrectEqualsAndHash);
+    }
+  }
+
+  private static class IncorrectHash implements Serializable {
+
+    public String name;
+    public int num;
+
+    public IncorrectHash(String name, int num) {
+      this.name = name;
+      this.num = num;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      IncorrectHash foo = (IncorrectHash)o;
+      return Objects.equal(name, foo.name) && Objects.equal(num, foo.num);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(name);
+    }
+  }
+
+  private static class IncorrectEqualsAndHash implements Serializable {
+
+    public String name;
+    public int num;
+
+    public IncorrectEqualsAndHash(String name, int num) {
+      this.name = name;
+      this.num = num;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      IncorrectEqualsAndHash foo = (IncorrectEqualsAndHash)o;
+      return Objects.equal(name, foo.name);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(name);
+    }
+  }
+
+  private static class ClassWithIncorrectEqualsMember implements Serializable {
+
+    public String myName;
+    public IncorrectEqualsAndHash myIncorrectEqualsAndHash;
+
+    public ClassWithIncorrectEqualsMember(String name, String innerName, int innerNum) {
+      this.myName = name;
+      this.myIncorrectEqualsAndHash = new IncorrectEqualsAndHash(innerName, innerNum);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      ClassWithIncorrectEqualsMember that = (ClassWithIncorrectEqualsMember)o;
+      return Objects.equal(myName, that.myName) &&
+             Objects.equal(myIncorrectEqualsAndHash, that.myIncorrectEqualsAndHash);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(myName, myIncorrectEqualsAndHash);
+    }
+  }
+
+  private static class IncorrectEqualsWithArray implements Serializable {
+
+    public IncorrectEqualsAndHash[] array;
+
+    public IncorrectEqualsWithArray(IncorrectEqualsAndHash[] array) {
+      this.array = array;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      IncorrectEqualsWithArray that = (IncorrectEqualsWithArray)o;
+      return Arrays.equals(array, that.array);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode((Object[])array);
+    }
+  }
+
+  private static class SubIncorrectEqualsAndHash extends IncorrectEqualsAndHash {
+
+    public Long num;
+
+    public SubIncorrectEqualsAndHash(String name, int iNum, Long num) {
+      super(name, iNum);
+      this.num = num;
+    }
+  }
+
+  private static enum ENUMS {
+    ONE, TWO, THREE
+  }
+
+  private static class DeepClass<T> implements Serializable {
+
+    public ENUMS myEnum;
+    public char myC;
+    public T data;
+    public File f;
+
+    public DeepClass(ENUMS myEnum, char c, T data, File f) {
+      this.myEnum = myEnum;
+      this.myC = c;
+      this.data = data;
+      this.f = f;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      DeepClass<?> deepClass = (DeepClass<?>)o;
+      return Objects.equal(myC, deepClass.myC) &&
+             Objects.equal(myEnum, deepClass.myEnum) &&
+             Objects.equal(data, deepClass.data) &&
+             Objects.equal(f, deepClass.f);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(myEnum, myC, data, f);
+    }
+  }
+
+  private static class SimpleClassWithSet implements Serializable {
+
+    public Set<File> files;
+
+    public SimpleClassWithSet(Set<File> files) {
+      this.files = files;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      SimpleClassWithSet that = (SimpleClassWithSet)o;
+      return Objects.equal(files, that.files);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(files);
+    }
+  }
+
+  private static class MapWithIncorrectKey implements Serializable {
+
+    public Map<IncorrectEqualsAndHash, File> files;
+
+    public MapWithIncorrectKey(Map<IncorrectEqualsAndHash, File> files) {
+      this.files = files;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      MapWithIncorrectKey that = (MapWithIncorrectKey)o;
+      return Objects.equal(files, that.files);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(files);
+    }
+  }
+
+  private static class MapWithIncorrectValue implements Serializable {
+
+    public Map<String, IncorrectEqualsAndHash> map;
+
+    public MapWithIncorrectValue(Map<String, IncorrectEqualsAndHash> map) {
+      this.map = map;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      MapWithIncorrectValue that = (MapWithIncorrectValue)o;
+      return Objects.equal(map, that.map);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(map);
+    }
+  }
+
+  private static class MapWithCorrectKeyAndValues implements Serializable {
+
+    public Map<String, String> map;
+
+    public MapWithCorrectKeyAndValues(Map<String, String> map) {
+      this.map = map;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      MapWithCorrectKeyAndValues that = (MapWithCorrectKeyAndValues)o;
+      return Objects.equal(map, that.map);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(map);
+    }
+  }
+
+  private Examples testExamples;
+
+  @Before
+  public void populateExtraExamples() {
+    testExamples = new Examples();
+    testExamples.addExample(CorrectEqualsAndHash.class, new CorrectEqualsAndHash("A"),
+                            new CorrectEqualsAndHash("B"));
+    testExamples.addExample(IncorrectEqualsAndHash.class, new IncorrectEqualsAndHash("A", 100),
+                            new IncorrectEqualsAndHash("A", 200));
+    testExamples
+      .addExample(IncorrectHash.class, new IncorrectHash("A", 100), new IncorrectHash("A", 200));
+  }
+
+  @Test
+  public void testCorrectEqualsAndHashPassesTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    CorrectEqualsAndHash myFoo = new CorrectEqualsAndHash("test");
+    DeepEqualsTester.doDeepEqualsAndHashTest(myFoo, testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testIncorrectEqualsFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    IncorrectEqualsAndHash myFoo = new IncorrectEqualsAndHash("test", 4);
+    DeepEqualsTester.doDeepEqualsAndHashTest(myFoo, testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testIncorrectHashFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    IncorrectHash myFoo = new IncorrectHash("test", 4);
+    DeepEqualsTester.doDeepEqualsAndHashTest(myFoo, testExamples);
+  }
+
+  @Test
+  public void testCorrectDeepEqualsAndHashPassesTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    ClassWithCorrectEqualsMember myFoo = new ClassWithCorrectEqualsMember("test", "inner test");
+    DeepEqualsTester.doDeepEqualsAndHashTest(myFoo, testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testDeepIncorrectEqualsFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    ClassWithIncorrectEqualsMember myFoo = new ClassWithIncorrectEqualsMember("test", "inner test",
+                                                                              4);
+    DeepEqualsTester.doDeepEqualsAndHashTest(myFoo, testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testIncorrectEqualsInSuperclassFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    SubIncorrectEqualsAndHash myFoo = new SubIncorrectEqualsAndHash("test", 4, new Long(39903));
+    DeepEqualsTester.doDeepEqualsAndHashTest(myFoo, testExamples);
+  }
+
+  @Test
+  public void testCorrectEqualsAndHashInArrayPassesTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    CorrectEqualsAndHash myFoo = new CorrectEqualsAndHash("test");
+    CorrectEqualsAndHash[] array = new CorrectEqualsAndHash[1];
+    array[0] = myFoo;
+    DeepEqualsTester.doDeepEqualsAndHashTest(array, testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testIncorrectEqualsInArrayFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    IncorrectEqualsAndHash myFoo = new IncorrectEqualsAndHash("test", 4);
+    IncorrectEqualsAndHash[] array = new IncorrectEqualsAndHash[1];
+    array[0] = myFoo;
+    IncorrectEqualsWithArray toTest = new IncorrectEqualsWithArray(array);
+    DeepEqualsTester.doDeepEqualsAndHashTest(toTest, testExamples);
+  }
+
+  @Test
+  public void testClassWithSetWithCorrectDeepEqualsAndHashPassesTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    Set<File> myFiles = Sets.newHashSet();
+    myFiles.add(new File("foo"));
+    myFiles.add(new File("bar"));
+    SimpleClassWithSet myFoo = new SimpleClassWithSet(myFiles);
+    DeepEqualsTester.doDeepEqualsAndHashTest(myFoo, testExamples);
+  }
+
+  @Ignore("causes java reflection return a type variable instead of a concrete type")
+  @Test
+  public void testCorrectEqualsAndHashInSetPassesTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    CorrectEqualsAndHash myFoo = new CorrectEqualsAndHash("test");
+    HashSet<CorrectEqualsAndHash> set = Sets.newHashSet();
+    set.add(myFoo);
+    DeepEqualsTester.doDeepEqualsAndHashTest(new Box<HashSet<CorrectEqualsAndHash>>(set),
+                                             testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testIncorrectEqualsInSetFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    IncorrectEqualsAndHash myFoo = new IncorrectEqualsAndHash("test", 4);
+    HashSet<IncorrectEqualsAndHash> set = Sets.newHashSet();
+    set.add(myFoo);
+    DeepEqualsTester.doDeepEqualsAndHashTest(new Box<HashSet<IncorrectEqualsAndHash>>(set),
+                                             testExamples);
+  }
+
+  @Ignore("causes java reflection return a type variable instead of a concrete type")
+  @Test
+  public void testCorrectDeepEqualsAndHashInSetPassesTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    CorrectEqualsAndHash myFoo = new CorrectEqualsAndHash("test");
+    DeepClass<CorrectEqualsAndHash> data = new DeepClass<CorrectEqualsAndHash>(ENUMS.THREE, 'z',
+                                                                               myFoo,
+                                                                               new File("home"));
+    HashSet<DeepClass<CorrectEqualsAndHash>> set = Sets.newHashSet();
+    set.add(data);
+    DeepEqualsTester
+      .doDeepEqualsAndHashTest(new Box<HashSet<DeepClass<CorrectEqualsAndHash>>>(set),
+                               testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testIncorrectDeepEqualsInSetFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    IncorrectEqualsAndHash myFoo = new IncorrectEqualsAndHash("test", 4);
+    DeepClass<IncorrectEqualsAndHash> data = new DeepClass<IncorrectEqualsAndHash>(ENUMS.ONE, 'e',
+                                                                                   myFoo,
+                                                                                   new File("home"));
+    HashSet<DeepClass<IncorrectEqualsAndHash>> set = Sets.newHashSet();
+    set.add(data);
+    DeepEqualsTester
+      .doDeepEqualsAndHashTest(new Box<HashSet<DeepClass<IncorrectEqualsAndHash>>>(set),
+                               testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testIncorrectEqualsForMapKeyFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    IncorrectEqualsAndHash myFoo = new IncorrectEqualsAndHash("test", 4);
+    Map<IncorrectEqualsAndHash, File> map = Maps.newHashMap();
+    map.put(myFoo, new File("file"));
+    MapWithIncorrectKey data = new MapWithIncorrectKey(map);
+    DeepEqualsTester.doDeepEqualsAndHashTest(data, testExamples);
+  }
+
+  @Test(expected = AssertionError.class)
+  public void testIncorrectEqualsForMapValueFailsTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    IncorrectEqualsAndHash myFoo = new IncorrectEqualsAndHash("test", 4);
+    Map<String, IncorrectEqualsAndHash> map = Maps.newHashMap();
+    map.put("first", myFoo);
+    MapWithIncorrectValue data = new MapWithIncorrectValue(map);
+    DeepEqualsTester.doDeepEqualsAndHashTest(data, testExamples);
+  }
+
+  @Test
+  public void testCorrectEqualsAndHashForMapKeyValuePassesTester()
+    throws IllegalAccessException, InstantiationException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    Map<String, String> map = Maps.newHashMap();
+    map.put("key", "value");
+    MapWithCorrectKeyAndValues data = new MapWithCorrectKeyAndValues(map);
+    DeepEqualsTester.doDeepEqualsAndHashTest(data, testExamples);
+  }
+
+  @Test
+  public void testEqualsAfterCloneReturnsTrue() {
+    CorrectEqualsAndHash myData = new CorrectEqualsAndHash("my data");
+    CorrectEqualsAndHash clone = (CorrectEqualsAndHash)DeepEqualsTesterUtil
+      .cloneWithSerialization(myData);
+    Assert.assertEquals(myData, clone);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTester.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTester.java
new file mode 100644
index 0000000..bb8b982
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTester.java
@@ -0,0 +1,139 @@
+/*
+ * 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.model.blaze.deepequalstester;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.blaze.deepequalstester.Examples.ExampleNotFoundException;
+import com.google.idea.blaze.base.model.blaze.deepequalstester.Examples.Pair;
+import com.google.idea.blaze.base.model.blaze.deepequalstester.ReachabilityAnalysis.ReachableClasses;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Assert;
+
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class DeepEqualsTester {
+
+  public static class TestCorrectnessException extends Exception {
+
+    public TestCorrectnessException(String s) {
+      super(s);
+    }
+  }
+
+  /**
+   * Ensure that the equals method of {@param rootObject} uses all of its fields in its comparison.
+   * Recurse into every field of {@param rootObject} to ensure that they also use all of their
+   * fields in their equals method. Continue recursion until primitives are hit.
+   * <p/>
+   * If multiple failures could occur, there is no guarantee that they will always occur in the same
+   * order. Only the first failure is reported.
+   *
+   * @param rootObject an example instantiation of the class we want to test for deep equals sanity.
+   *                   The object must be a standard java object (no collections, no arrays, no primitives). If you
+   *                   would like to pass these types in, you should put them in a box first.
+   * @param examples   examples of objects to use for comparison. This should contain a pair of
+   *                   examples for every type that could be reachable from the root object. This value may be
+   *                   mutated by this test.
+   */
+  public static <T extends Serializable> void doDeepEqualsAndHashTest(@NotNull T rootObject,
+                                                                      Examples examples)
+    throws InstantiationException, IllegalAccessException, NoSuchFieldException, ExampleNotFoundException, TestCorrectnessException {
+    ReachableClasses reachableClasses = new ReachableClasses();
+
+    try {
+      ArrayList<String> initialPath = Lists.newArrayList("root");
+      // Find all of the classes reachable from the root object. This is not sound since it
+      // ignores subtypes (or supertypes) that could be used
+      ReachabilityAnalysis
+        .computeReachableFromObject(rootObject, rootObject.getClass(), initialPath,
+                                    reachableClasses);
+    }
+    catch (ClassNotFoundException e) {
+      e.printStackTrace();
+    }
+
+    // Add the root object to our list of reachable classes so we can do all the testing in one
+    // loop
+    reachableClasses.addPath(rootObject.getClass(), Lists.newArrayList("root"));
+    // In our situations, we never need a second example of the root object
+    examples.addExample(rootObject.getClass(), rootObject, rootObject);
+    // For each reachable class, do a shallow equals test where we change each value of the
+    // object one at a time and test for equality
+    for (Class<? extends Serializable> clazz : reachableClasses.getClasses()) {
+      Serializable workitem = (Serializable)examples.getExamples(clazz).getFirst();
+      testShallowEquals(workitem, reachableClasses, examples);
+    }
+  }
+
+  private static String getFailureMessage(String method, Field field, List<String> examplePath) {
+    StringBuilder sb = new StringBuilder();
+    sb.append(field.toString()).append(" is not represented in it's parent's ")
+      .append(method).append(" method\n");
+    for (String path : examplePath) {
+      sb.append("\t").append(path).append("\n");
+    }
+    sb.append("\n");
+    return sb.toString();
+  }
+
+  /**
+   * Mutate each field in the object one at a time and test for equality of the object. Assert a
+   * failure if any mutation doesn't result in the two objects not being equal
+   */
+  private static <T extends Serializable> void testShallowEquals(@NotNull T original,
+                                                                 ReachableClasses reachableClasses, Examples examples)
+    throws ExampleNotFoundException, IllegalAccessException, TestCorrectnessException {
+    T clone = (T)DeepEqualsTesterUtil.cloneWithSerialization(original);
+    List<Field> allFields = DeepEqualsTesterUtil.getAllFields(original.getClass());
+    for (Field field : allFields) {
+      if (!Modifier.isStatic(field.getModifiers())) {
+        field.setAccessible(true);
+        Pair<?, ?> examplesPair = examples.
+          getExamples((Class<? extends Serializable>)field.getType());
+        Object newValueForOriginal = examplesPair.getFirst();
+        Object newValueForClone = examplesPair.getSecond();
+        Object oldValueForOriginal = field.get(original);
+        Object oldValueForClone = field.get(clone);
+        // Ensure that the two objects really are equal before we tweak them
+        boolean objectsTheSameBeforeTweak = original.equals(clone);
+        if (!objectsTheSameBeforeTweak) {
+          throw new TestCorrectnessException(
+            "original was not equal to clone before tweaking them");
+        }
+        boolean objectsHashTheSameBeforeTweak = original.hashCode() == clone.hashCode();
+        if (!objectsHashTheSameBeforeTweak) {
+          throw new TestCorrectnessException(
+            "original hash code was not equal to clone hash code before tweaking the objects");
+        }
+        field.set(original, newValueForOriginal);
+        field.set(clone, newValueForClone);
+        boolean equalsWorksAsIntended = !original.equals(clone);
+        boolean hashWorksAsIntended = original.hashCode() != clone.hashCode();
+        // Return to our original state before possibly failing
+        field.set(original, oldValueForOriginal);
+        field.set(clone, oldValueForClone);
+        Assert.assertTrue(getFailureMessage("equals", field, reachableClasses.getExamplePathTo(
+          original.getClass())), equalsWorksAsIntended);
+        Assert.assertTrue(getFailureMessage("hash", field, reachableClasses.getExamplePathTo(
+          original.getClass())), hashWorksAsIntended);
+      }
+    }
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java
new file mode 100644
index 0000000..71f0c6d
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/DeepEqualsTesterUtil.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.model.blaze.deepequalstester;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.lang.reflect.Field;
+import java.util.List;
+
+@VisibleForTesting
+public final class DeepEqualsTesterUtil {
+  public static List<Field> getAllFields(Class<?> clazz) {
+    List<Field> fields = Lists.newArrayList();
+
+    Field[] declaredFields = clazz.getDeclaredFields();
+    for (Field field : declaredFields) {
+      fields.add(field);
+    }
+
+    Class<?> superclass = clazz.getSuperclass();
+    if (superclass != null) {
+      fields.addAll(getAllFields(superclass));
+    }
+    return fields;
+  }
+
+  public static Class getClass(Class declaredClass, Object o) {
+    if (o == null) {
+      return declaredClass;
+    }
+    // The two classes *should* be the same in this case, but the class from o.getClass won't
+    // return true from isPrimitive
+    if (declaredClass.isPrimitive()) {
+      return declaredClass;
+    }
+    return o.getClass();
+  }
+
+  /**
+   * Do a deep clone of an object using reflection
+   */
+  public static Object cloneWithSerialization(Object o) {
+    if (o == null) {
+      return null;
+    }
+
+    try {
+      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+      ObjectOutputStream objOut = new ObjectOutputStream(outputStream);
+      objOut.writeObject(o);
+      ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());
+      ObjectInputStream objIn = new ObjectInputStream(inputStream);
+      return objIn.readObject();
+    }
+    catch (Exception e) {
+      return null;
+    }
+  }
+
+  public static boolean isSubclassOf(@Nullable Class<?> possibleSubClass, @NotNull Class<?> possibleSuperClass) {
+    if (possibleSubClass == null) {
+      return false;
+    }
+
+    if (possibleSubClass.equals(possibleSuperClass)) {
+      return true;
+    }
+
+    if (possibleSubClass.equals(Object.class)) {
+      return false;
+    }
+
+    Class<?>[] interfaces = possibleSubClass.getInterfaces();
+    for (Class<?> interfaze : interfaces) {
+      if (interfaze.equals(possibleSuperClass)) {
+        return true;
+      }
+    }
+
+    return isSubclassOf(possibleSubClass.getSuperclass(), possibleSuperClass);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/Examples.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/Examples.java
new file mode 100644
index 0000000..c989196
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/Examples.java
@@ -0,0 +1,187 @@
+/*
+ * 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.model.blaze.deepequalstester;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.jetbrains.annotations.NotNull;
+
+import java.io.File;
+import java.io.Serializable;
+import java.lang.reflect.Array;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static com.google.idea.blaze.base.model.blaze.deepequalstester.DeepEqualsTesterUtil.isSubclassOf;
+
+public final class Examples {
+
+  public static final class Pair<First, Second> {
+
+    private final First first;
+    private final Second second;
+
+    public static <First, Second> Pair<First, Second> of(First first, Second second) {
+      return new Pair<First, Second>(first, second);
+    }
+
+    public Pair(First first, Second second) {
+      this.first = first;
+      this.second = second;
+    }
+
+    First getFirst() {
+      return first;
+    }
+
+    Second getSecond() {
+      return second;
+    }
+  }
+
+  private static Map<Class<? extends Object>, Pair<? extends Object, ? extends Object>> BASE_EXAMPLES;
+  private static Map<Class<? extends Object>, Pair<? extends Object, ? extends Object>> ARRAY_EXAMPLES;
+
+  private final Map<Class<? extends Object>, Pair<? extends Object, ? extends Object>> customExamples;
+
+  static {
+    BASE_EXAMPLES = Maps.newHashMap();
+    BASE_EXAMPLES.put(String.class, Pair.of("foo", "foobar"));
+    BASE_EXAMPLES.put(File.class, Pair.of(new File("a"), new File("b")));
+    BASE_EXAMPLES.put(Integer.class, Pair.of(1, 2));
+    BASE_EXAMPLES.put(Integer.TYPE, Pair.of(1, 2));
+    BASE_EXAMPLES.put(Long.class, Pair.of(1L, 2L));
+    BASE_EXAMPLES.put(Long.TYPE, Pair.of(1L, 2L));
+    BASE_EXAMPLES.put(Short.class, Pair.of((short)1, (short)2));
+    BASE_EXAMPLES.put(Short.TYPE, Pair.of((short)1, (short)2));
+    BASE_EXAMPLES.put(Character.class, Pair.of('a', 'b'));
+    BASE_EXAMPLES.put(Character.TYPE, Pair.of('a', 'b'));
+    BASE_EXAMPLES.put(Byte.class, Pair.of((byte)1, (byte)2));
+    BASE_EXAMPLES.put(Byte.TYPE, Pair.of((byte)1, (byte)2));
+    BASE_EXAMPLES.put(Float.class, Pair.of((float)1.0, (float)2.0));
+    BASE_EXAMPLES.put(Float.TYPE, Pair.of((float)1.0, (float)2.0));
+    BASE_EXAMPLES.put(Double.class, Pair.of(1.0, 2.0));
+    BASE_EXAMPLES.put(Double.TYPE, Pair.of(1.0, 2.0));
+    BASE_EXAMPLES.put(Boolean.class, Pair.of(true, false));
+    BASE_EXAMPLES.put(Boolean.TYPE, Pair.of(true, false));
+    Map<String, String> mapA = Maps.newHashMap();
+    mapA.put("foo", "bar");
+    Map<String, String> mapB = Maps.newHashMap();
+    mapB.put("tip", "top");
+    BASE_EXAMPLES.put(Map.class, Pair.of(mapA, mapB));
+    Set<String> setA = new ImmutableSet.Builder<String>().add("A").build();
+    Set<String> setB = new ImmutableSet.Builder<String>().add("A").add("B").build();
+    BASE_EXAMPLES.put(Set.class, Pair.of(setA, setB));
+    BASE_EXAMPLES.put(Collection.class, Pair.of(setA, setB));
+    Builder<String> listABuilder = ImmutableList.builder();
+    List<String> listA = listABuilder.add("A").build();
+    Builder<String> listBBuilder = ImmutableList.builder();
+    List<String> listB = listBBuilder.add("A").add("B").build();
+    BASE_EXAMPLES.put(List.class, Pair.of(listA, listB));
+
+    ARRAY_EXAMPLES = Maps.newHashMap();
+    int[] intArrA = {1, 2};
+    int[] intArrB = {3, 4};
+    ARRAY_EXAMPLES.put(Integer.TYPE, Pair.of(intArrA, intArrB));
+    long[] longArrA = {1, 2};
+    long[] longArrB = {3, 4};
+    ARRAY_EXAMPLES.put(Long.TYPE, Pair.of(longArrA, longArrB));
+    short[] shortArrA = {1, 2};
+    short[] shortArrB = {3, 4};
+    ARRAY_EXAMPLES.put(Short.TYPE, Pair.of(shortArrA, shortArrB));
+    char[] charArrA = {'a', 'b'};
+    char[] charArrB = {'c', 'd'};
+    ARRAY_EXAMPLES.put(Character.TYPE, Pair.of(charArrA, charArrB));
+    byte[] byteArrA = {1, 2};
+    byte[] byteArrB = {3, 4};
+    ARRAY_EXAMPLES.put(Byte.TYPE, Pair.of(byteArrA, byteArrB));
+    boolean[] boolArrA = {true, false};
+    boolean[] boolArrB = {false, false};
+    ARRAY_EXAMPLES.put(Boolean.TYPE, Pair.of(boolArrA, boolArrB));
+    float[] floatArrA = {1.0f, 2.0f};
+    float[] floatArrB = {3.0f, 4.0f};
+    ARRAY_EXAMPLES.put(Float.TYPE, Pair.of(floatArrA, floatArrB));
+    double[] doubleArrA = {1.0, 2.0};
+    double[] doubleArrB = {3.0, 4.0};
+    ARRAY_EXAMPLES.put(Double.TYPE, Pair.of(doubleArrA, doubleArrB));
+  }
+
+  public static class ExampleNotFoundException extends Exception {
+
+    private final Class<?> clazz;
+
+    public ExampleNotFoundException(Class<?> clazz) {
+      this.clazz = clazz;
+    }
+
+    @Override
+    public String getMessage() {
+      return "Could not find example for: " + clazz.toString();
+    }
+  }
+
+  public Examples() {
+    this.customExamples = Maps.newHashMap();
+  }
+
+  public void addExample(Class<? extends Object> clazz, Object a, Object b) {
+    customExamples.put(clazz, Pair.of(a, b));
+  }
+
+  public <T extends Serializable> Pair<? extends Object, ? extends Object> getExamples(
+    @NotNull Class<T> clazz)
+    throws ExampleNotFoundException {
+    if (customExamples.containsKey(clazz)) {
+      return customExamples.get(clazz);
+    }
+    if (BASE_EXAMPLES.containsKey(clazz)) {
+      return BASE_EXAMPLES.get(clazz);
+    }
+    // Special case subclasses of collections
+    if (isSubclassOf(clazz, Set.class)) {
+      return BASE_EXAMPLES.get(Set.class);
+    }
+    if (isSubclassOf(clazz, List.class)) {
+      return BASE_EXAMPLES.get(List.class);
+    }
+    if (isSubclassOf(clazz, Collection.class)) {
+      return BASE_EXAMPLES.get(Collection.class);
+    }
+
+    // If we have an array of Objects, we have to do a little trickery to create one we can swap
+    // in
+    if (clazz.isArray()) {
+      Class<?> arrayType = clazz.getComponentType();
+      if (!arrayType.isPrimitive()) {
+        Pair<?, ?> examples = getExamples((Class<? extends Serializable>)arrayType);
+        Object arrayA = Array.newInstance(arrayType, 1);
+        Array.set(arrayA, 0, examples.getFirst());
+        Object arrayB = Array.newInstance(arrayType, 1);
+        Array.set(arrayB, 0, examples.getSecond());
+        return Pair.of(arrayA, arrayB);
+      }
+      if (ARRAY_EXAMPLES.containsKey(arrayType)) {
+        return ARRAY_EXAMPLES.get(arrayType);
+      }
+    }
+
+    throw new ExampleNotFoundException(clazz);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/ReachabilityAnalysis.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/ReachabilityAnalysis.java
new file mode 100644
index 0000000..f8485c8
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/blaze/deepequalstester/ReachabilityAnalysis.java
@@ -0,0 +1,188 @@
+/*
+ * 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.model.blaze.deepequalstester;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import org.junit.Assert;
+
+import java.io.File;
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public final class ReachabilityAnalysis {
+
+  /**
+   * Wrapper around a map from class to set of paths that lead to that path from the root object
+   */
+  public static final class ReachableClasses {
+
+    Map<Class<? extends Serializable>, Set<List<String>>> map;
+
+    public ReachableClasses() {
+      map = Maps.newHashMap();
+    }
+
+    boolean alreadyFound(Class<? extends Serializable> clazz) {
+      return map.containsKey(clazz);
+    }
+
+    void addPath(Class<? extends Serializable> clazz, List<String> path) {
+      Set<List<String>> paths;
+      if (map.containsKey(clazz)) {
+        paths = map.get(clazz);
+      }
+      else {
+        paths = Sets.newHashSet();
+        map.put(clazz, paths);
+      }
+      paths.add(path);
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder sb = new StringBuilder();
+
+      Set<? extends Entry<Class<? extends Serializable>, ? extends Set<? extends List<String>>>> entries = map
+        .entrySet();
+      for (Entry<Class<? extends Serializable>, ? extends Set<? extends List<String>>> entry : entries) {
+        sb.append(entry.getKey().toString());
+        sb.append("\n");
+      }
+
+      return sb.toString();
+    }
+
+    public Set<Class<? extends Serializable>> getClasses() {
+      return map.keySet();
+    }
+
+    public List<String> getExamplePathTo(Class<? extends Serializable> aClass) {
+      if (map.containsKey(aClass)) {
+        return map.get(aClass).iterator().next();
+      }
+      return Lists.newArrayList();
+    }
+  }
+
+  /**
+   * Find all of the classes reachable from a root object
+   *
+   * @param root              object to start reachability calculation from
+   * @param declaredRootClass declared class of the root object
+   * @param currentPath       field access path to get to root
+   * @param reachableClasses  output: add classes reachable from root to this object
+   * @throws IllegalAccessException
+   * @throws ClassNotFoundException
+   */
+  public static void computeReachableFromObject(Object root, Class<?> declaredRootClass,
+                                                List<String> currentPath, ReachableClasses reachableClasses)
+    throws IllegalAccessException, ClassNotFoundException {
+    final Class<?> concreteRootClass = DeepEqualsTesterUtil.getClass(declaredRootClass, root);
+    List<Field> allFields = DeepEqualsTesterUtil.getAllFields(concreteRootClass);
+    for (Field field : allFields) {
+      if (!Modifier.isStatic(field.getModifiers())) {
+        field.setAccessible(true);
+        final Object fieldObject;
+        if (root == null) {
+          fieldObject = null;
+        }
+        else {
+          fieldObject = field.get(root);
+        }
+        List<String> childPath = Lists.newArrayList();
+        childPath.addAll(currentPath);
+        childPath.add(field.toString());
+        addToReachableAndRecurse(fieldObject, field.getType(), field.getGenericType(),
+                                 childPath, reachableClasses);
+      }
+    }
+  }
+
+  /**
+   * Determine the action we should take based on the type of Object and then take it. In the
+   * normal object case, this results in a recursive call to
+   * {@link #computeReachableFromObject(Object, Class, List, ReachableClasses)}. In the case of
+   * Collections, we skip the Collection type and continue on with the type contained in the
+   * collection.
+   */
+  private static void addToReachableAndRecurse(Object object, Class<?> declaredObjectClass,
+                                               Type genericType, List<String> currentPath, ReachableClasses reachableClasses)
+    throws ClassNotFoundException, IllegalAccessException {
+    Class<? extends Serializable> objectType = DeepEqualsTesterUtil
+      .getClass(declaredObjectClass, object);
+    // TODO(salguarnieri) modify if so all ignored classes are taken care of together
+    if (objectType.isPrimitive()) {
+      // ignore
+    }
+    else if (objectType.isEnum()) {
+      // assume enums do the right thing, ignore
+    }
+    else if (DeepEqualsTesterUtil.isSubclassOf(objectType, String.class)) {
+      // ignore
+    }
+    else if (DeepEqualsTesterUtil.isSubclassOf(objectType, File.class)) {
+      // ignore
+    }
+    else if (DeepEqualsTesterUtil.isSubclassOf(objectType, Collection.class) || DeepEqualsTesterUtil
+      .isSubclassOf(objectType, Map.class)) {
+      if (genericType instanceof ParameterizedType) {
+        ParameterizedType parameterType = (ParameterizedType)genericType;
+        Type[] actualTypeArguments = parameterType.getActualTypeArguments();
+        for (Type typeArgument : actualTypeArguments) {
+          if (typeArgument instanceof Class) {
+            List<String> childPath = Lists.newArrayList();
+            childPath.addAll(currentPath);
+            childPath.add("[[IN COLLECTION]]");
+            // this does not properly handle subtyping
+            addToReachableAndRecurse(null, (Class)typeArgument, null, childPath,
+                                     reachableClasses);
+          }
+          else {
+            Assert.fail("This case is not handled yet");
+          }
+        }
+      }
+      else {
+        Assert.fail("This case is not handled yet");
+      }
+    }
+    else if (objectType.isArray()) {
+      Class<?> typeInArray = objectType.getComponentType();
+      // This does not properly handle subtyping
+      List<String> childPath = Lists.newArrayList();
+      childPath.addAll(currentPath);
+      childPath.add("[[IN ARRAY]]");
+      addToReachableAndRecurse(null, typeInArray, null, childPath, reachableClasses);
+    }
+    else {
+      boolean doRecursion = !reachableClasses.alreadyFound(objectType);
+      reachableClasses.addPath(objectType, currentPath);
+      if (doRecursion) {
+        computeReachableFromObject(object, declaredObjectClass, currentPath, reachableClasses);
+      }
+    }
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/ExecutionRootPathTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/ExecutionRootPathTest.java
new file mode 100644
index 0000000..b0426fb
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/ExecutionRootPathTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.model.primitives;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class ExecutionRootPathTest extends BlazeTestCase {
+  @Test
+  public void testSingleLevelPathEndInSlash() {
+    ExecutionRootPath executionRootPath = new ExecutionRootPath("foo");
+    assertThat(executionRootPath.getAbsoluteOrRelativeFile()).isEqualTo(new File("foo/"));
+
+    ExecutionRootPath executionRootPath2 = new ExecutionRootPath("foo/");
+    assertThat(executionRootPath2.getAbsoluteOrRelativeFile()).isEqualTo(new File("foo/"));
+  }
+
+  @Test
+  public void testMultiLevelPathEndInSlash() {
+    ExecutionRootPath executionRootPath = new ExecutionRootPath("foo/bar");
+    assertThat(executionRootPath.getAbsoluteOrRelativeFile()).isEqualTo(new File("foo/bar/"));
+
+    ExecutionRootPath executionRootPath2 = new ExecutionRootPath("foo/bar/");
+    assertThat(executionRootPath2.getAbsoluteOrRelativeFile()).isEqualTo(new File("foo/bar/"));
+  }
+
+  @Test
+  public void testAbsoluteFileDoesNotGetRerooted() {
+    ExecutionRootPath executionRootPath = new ExecutionRootPath("/root/foo/bar");
+    File rootedFile = executionRootPath.getFileRootedAt(new File("/core/dev"));
+    assertThat(rootedFile).isEqualTo(new File("/root/foo/bar"));
+  }
+
+  @Test
+  public void testRelativeFileGetsRerooted() {
+    ExecutionRootPath executionRootPath = new ExecutionRootPath("foo/bar");
+    File rootedFile = executionRootPath.getFileRootedAt(new File("/root"));
+    assertThat(rootedFile).isEqualTo(new File("/root/foo/bar"));
+  }
+
+  @Test
+  public void testCreateRelativePathWithTwoRelativePaths() {
+    ExecutionRootPath relativePathFragment = ExecutionRootPath.createAncestorRelativePath(
+      createMockDirectory("code/lib/fastmath"),
+      createMockDirectory("code/lib/fastmath/lib1")
+    );
+    assertThat(relativePathFragment).isNotNull();
+    assertThat(relativePathFragment.getAbsoluteOrRelativeFile()).isEqualTo(new File("lib1"));
+  }
+
+  @Test
+  public void testCreateRelativePathWithTwoRelativePathsWithNoRelativePath() {
+    ExecutionRootPath relativePathFragment = ExecutionRootPath.createAncestorRelativePath(
+      createMockDirectory("obj/lib/fastmath"),
+      createMockDirectory("code/lib/slowmath")
+    );
+    assertThat(relativePathFragment).isNull();
+  }
+
+  @Test
+  public void testCreateRelativePathWithTwoAbsolutePaths() {
+    ExecutionRootPath relativePathFragment = ExecutionRootPath.createAncestorRelativePath(
+      createMockDirectory("/code/lib/fastmath"),
+      createMockDirectory("/code/lib/fastmath/lib1")
+    );
+    assertThat(relativePathFragment).isNotNull();
+    assertThat(relativePathFragment.getAbsoluteOrRelativeFile()).isEqualTo(new File("lib1"));
+  }
+
+  @Test
+  public void testCreateRelativePathWithTwoAbsolutePathsWithNoRelativePath() {
+    ExecutionRootPath relativePathFragment = ExecutionRootPath.createAncestorRelativePath(
+      createMockDirectory("/obj/lib/fastmath"),
+      createMockDirectory("/code/lib/slowmath")
+    );
+    assertThat(relativePathFragment).isNull();
+  }
+
+  @Test
+  public void testCreateRelativePathWithOneAbsolutePathAndOneRelativePathReturnsNull1() {
+    ExecutionRootPath relativePathFragment = ExecutionRootPath.createAncestorRelativePath(
+      createMockDirectory("/code/lib/fastmath"),
+      createMockDirectory("code/lib/fastmath/lib1")
+    );
+    assertThat(relativePathFragment).isNull();
+  }
+
+  @Test
+  public void testCreateRelativePathWithOneAbsolutePathAndOneRelativePathReturnsNull2() {
+    ExecutionRootPath relativePathFragment = ExecutionRootPath.createAncestorRelativePath(
+      createMockDirectory("code/lib/fastmath"),
+      createMockDirectory("/code/lib/slowmath")
+    );
+    assertThat(relativePathFragment).isNull();
+  }
+
+  private static File createMockDirectory(String path) {
+    File org = new File(path);
+    File spy = Mockito.spy(org);
+    Mockito.when(spy.isDirectory()).then((Answer<Boolean>)invocationOnMock -> true);
+    return spy;
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/LabelTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/LabelTest.java
new file mode 100644
index 0000000..a865ff1
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/LabelTest.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.model.primitives;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class LabelTest extends BlazeTestCase {
+
+  @Override
+  protected void initTest(
+    @NotNull Container applicationServices,
+    @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+  }
+
+  @Test
+  public void testValidatePackage() {
+    // Legal names
+    assertThat(Label.validatePackagePath("foo")).isTrue();
+    assertThat(Label.validatePackagePath("f")).isTrue();
+    assertThat(Label.validatePackagePath("fooBAR")).isTrue();
+    assertThat(Label.validatePackagePath("foo/bar")).isTrue();
+    assertThat(Label.validatePackagePath("f9oo")).isTrue();
+    assertThat(Label.validatePackagePath("f_9oo")).isTrue();
+    // This is not advised but is technically legal
+    assertThat(Label.validatePackagePath("")).isTrue();
+
+    // Illegal names
+    assertThat(Label.validatePackagePath("Foo")).isFalse();
+    assertThat(Label.validatePackagePath("foo//bar")).isFalse();
+    assertThat(Label.validatePackagePath("foo/")).isFalse();
+    assertThat(Label.validatePackagePath("9oo")).isFalse();
+  }
+
+  @Test
+  public void testValidateLabel() {
+    // Valid labels
+    assertThat(Label.validate("//foo:bar")).isTrue();
+    assertThat(Label.validate("//foo/baz:bar")).isTrue();
+    assertThat(Label.validate("//:bar")).isTrue();
+
+    // Invalid labels
+    assertThat(Label.validate("//foo")).isFalse();
+    assertThat(Label.validate("foo")).isFalse();
+    assertThat(Label.validate("foo:bar")).isFalse();
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/RuleNameTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/RuleNameTest.java
new file mode 100644
index 0000000..34ccbe7
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/RuleNameTest.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.model.primitives;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import org.junit.Test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class RuleNameTest extends BlazeTestCase {
+
+  @Test
+  public void testValidateRuleName() {
+    // Legal names
+    assertThat(RuleName.validate("foo")).isTrue();
+    assertThat(RuleName.validate(".")).isTrue();
+    assertThat(RuleName.validate(".foo")).isTrue();
+    assertThat(RuleName.validate("foo+")).isTrue();
+    assertThat(RuleName.validate("_foo")).isTrue();
+    assertThat(RuleName.validate("-foo")).isTrue();
+    assertThat(RuleName.validate("foo-bar")).isTrue();
+    assertThat(RuleName.validate("foo..")).isTrue();
+    assertThat(RuleName.validate("..foo")).isTrue();
+
+    // Illegal names
+    assertThat(RuleName.validate("")).isFalse();
+    assertThat(RuleName.validate("/foo")).isFalse();
+    assertThat(RuleName.validate("../foo")).isFalse();
+    assertThat(RuleName.validate("./foo")).isFalse();
+    assertThat(RuleName.validate("..")).isFalse();
+    assertThat(RuleName.validate("foo/../bar")).isFalse();
+    assertThat(RuleName.validate("foo/./bar")).isFalse();
+    assertThat(RuleName.validate("foo//bar")).isFalse();
+    assertThat(RuleName.validate("foo/..")).isFalse();
+    assertThat(RuleName.validate("/..")).isFalse();
+    assertThat(RuleName.validate("foo/")).isFalse();
+    assertThat(RuleName.validate("/")).isFalse();
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/TargetExpressionTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/TargetExpressionTest.java
new file mode 100644
index 0000000..a59a77b
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/TargetExpressionTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.model.primitives;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests for {@link com.google.idea.blaze.base.model.primitives.TargetExpressionFactory}.
+ */
+@RunWith(JUnit4.class)
+public class TargetExpressionTest extends BlazeTestCase {
+  @Override
+  protected void initTest(
+    @NotNull Container applicationServices,
+    @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+  }
+
+  @Test
+  public void validLabelShouldYieldLabel() {
+    TargetExpression target = TargetExpression.fromString("//package:rule");
+    assertThat(target).isInstanceOf(Label.class);
+  }
+
+  @Test
+  public void globExpressionShouldYieldGeneralTargetExpression() {
+    TargetExpression target = TargetExpression.fromString("//package/...");
+    assertThat(target.getClass()).isSameAs(TargetExpression.class);
+  }
+
+  @Test
+  public void emptyExpressionShouldThrow() {
+    try {
+      TargetExpression.fromString("");
+      fail("Empty expressions should not be allowed.");
+    }
+    catch (IllegalArgumentException expected) {
+    }
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/WorkspacePathTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/WorkspacePathTest.java
new file mode 100644
index 0000000..0e0c049
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/model/primitives/WorkspacePathTest.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.model.primitives;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.ui.BlazeValidationError;
+import org.junit.Test;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class WorkspacePathTest extends BlazeTestCase {
+
+  @Test
+  public void testValidation() {
+    // Valid workspace paths
+    assertThat(WorkspacePath.validate("")).isTrue();
+    assertThat(WorkspacePath.validate("foo")).isTrue();
+    assertThat(WorkspacePath.validate("foo")).isTrue();
+    assertThat(WorkspacePath.validate("foo/bar")).isTrue();
+    assertThat(WorkspacePath.validate("foo/bar/baz")).isTrue();
+
+    // Invalid workspace paths
+    assertThat(WorkspacePath.validate("/foo")).isFalse();
+    assertThat(WorkspacePath.validate("//foo")).isFalse();
+    assertThat(WorkspacePath.validate("/")).isFalse();
+    assertThat(WorkspacePath.validate("foo/")).isFalse();
+    assertThat(WorkspacePath.validate("foo:")).isFalse();
+    assertThat(WorkspacePath.validate(":")).isFalse();
+    assertThat(WorkspacePath.validate("foo:bar")).isFalse();
+
+
+    List<BlazeValidationError> errors = Lists.newArrayList();
+
+    WorkspacePath.validate("/foo", errors);
+    assertThat(errors.get(0).getError()).isEqualTo("Workspace path may not start with '/': /foo");
+    errors.clear();
+
+    WorkspacePath.validate("/", errors);
+    assertThat(errors.get(0).getError()).isEqualTo("Workspace path may not start with '/': /");
+    errors.clear();
+
+    WorkspacePath.validate("foo/", errors);
+    assertThat(errors.get(0).getError()).isEqualTo("Workspace path may not end with '/': foo/");
+    errors.clear();
+
+    WorkspacePath.validate("foo:bar", errors);
+    assertThat(errors.get(0).getError()).isEqualTo("Workspace path may not contain ':': foo:bar");
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java
new file mode 100644
index 0000000..0c295cf
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewSetTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.projectview;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.TestUtils;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.model.primitives.*;
+import com.google.idea.blaze.base.projectview.section.*;
+import com.google.idea.blaze.base.projectview.section.sections.*;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class ProjectViewSetTest extends BlazeTestCase {
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+    registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
+  }
+
+  @Test
+  public void testProjectViewSetSerializable() throws Exception {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY).add(DirectoryEntry.include(new WorkspacePath("test"))))
+             .put(ListSection.builder(TargetSection.KEY).add(TargetExpression.fromString("//test:all")))
+             .put(ScalarSection.builder(ImportSection.KEY).set(new WorkspacePath("test")))
+             .put(ListSection.builder(TestSourceSection.KEY).add(new Glob("javatests/*")))
+             .put(ListSection.builder(ExcludedSourceSection.KEY).add(new Glob("*.java")))
+             .put(ListSection.builder(BuildFlagsSection.KEY).add("--android_sdk=abcd"))
+             .put(ListSection.builder(ImportTargetOutputSection.KEY).add(new Label("//test:test")))
+             .put(ListSection.builder(ExcludeTargetSection.KEY).add(new Label("//test:test")))
+             .put(ScalarSection.builder(WorkspaceTypeSection.KEY).set(WorkspaceType.JAVA))
+             .put(ListSection.builder(AdditionalLanguagesSection.KEY).add(LanguageClass.JAVA))
+             .put(ScalarSection.builder(MetricsProjectSection.KEY).set("my project"))
+             .build())
+      .build();
+
+    // Assert we have all sections
+    assert projectViewSet.getTopLevelProjectViewFile() != null;
+    ProjectView projectView = projectViewSet.getTopLevelProjectViewFile().projectView;
+    for (SectionParser parser : Sections.getParsers()) {
+      Section section = projectView.getSectionOfType(parser.getSectionKey());
+      assertThat(section).isNotNull();
+    }
+
+    TestUtils.assertIsSerializable(projectViewSet);
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewVerifierTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewVerifierTest.java
new file mode 100644
index 0000000..c643521
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/ProjectViewVerifierTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.projectview;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.io.MockWorkspaceScanner;
+import com.google.idea.blaze.base.io.WorkspaceScanner;
+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.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.File;
+
+/**
+ * Tests for ProjectViewVerifier
+ */
+public class ProjectViewVerifierTest extends BlazeTestCase {
+
+  private String FAKE_ROOT = "/root";
+  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_ROOT));
+  private MockWorkspaceScanner workspaceScanner;
+  private ErrorCollector errorCollector = new ErrorCollector();
+  private BlazeContext context;
+  private WorkspaceLanguageSettings workspaceLanguageSettings = new WorkspaceLanguageSettings(
+    WorkspaceType.JAVA,
+    ImmutableSet.of( LanguageClass.JAVA)
+  );
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
+
+    workspaceScanner = new MockWorkspaceScanner();
+    applicationServices.register(WorkspaceScanner.class, workspaceScanner);
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+  }
+
+  @Test
+  public void testNoIssues() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+                    .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example2"))))
+             .build())
+      .build();
+    workspaceScanner.addProjectView(workspaceRoot, projectViewSet);
+    ProjectViewVerifier.verifyProjectView(context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+    errorCollector.assertNoIssues();
+  }
+
+  @Test
+  public void testExcludingExactRootResultsInIssue() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+                    .add(DirectoryEntry.exclude(new WorkspacePath("java/com/google/android/apps/example"))))
+             .build())
+      .build();
+    workspaceScanner.addProjectView(workspaceRoot, projectViewSet);
+    ProjectViewVerifier.verifyProjectView(context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+    errorCollector.assertIssues(
+      "java/com/google/android/apps/example is included, but that contradicts java/com/google/android/apps/example which was excluded");
+  }
+
+  @Test
+  public void testExcludingRootViaParentResultsInIssue() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+                    .add(DirectoryEntry.exclude(new WorkspacePath("java/com/google/android/apps"))))
+             .build())
+      .build();
+    workspaceScanner.addProjectView(workspaceRoot, projectViewSet);
+    ProjectViewVerifier.verifyProjectView(context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+    errorCollector.assertIssues(
+      "java/com/google/android/apps/example is included, but that contradicts java/com/google/android/apps which was excluded");
+  }
+
+  @Test
+  public void testExcludingSubdirectoryOfRootResultsInNoIssues() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+                    .add(DirectoryEntry.exclude(new WorkspacePath("java/com/google/android/apps/example/subdir"))))
+             .build())
+      .build();
+    workspaceScanner.addProjectView(workspaceRoot, projectViewSet);
+    ProjectViewVerifier.verifyProjectView(context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+    errorCollector.assertNoIssues();
+  }
+
+  @Test
+  public void testImportRootMissingResultsInIssue() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example"))))
+             .build())
+      .build();
+    ProjectViewVerifier.verifyProjectView(context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+    errorCollector.assertIssues(
+      String.format("Directory '%s' specified in import roots not found under workspace root '%s'",
+                    "java/com/google/android/apps/example", "/root"));
+  }
+
+  @Test
+  public void testOverlappingDirectoriesResultInIssue() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example"))))
+             .build())
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android"))))
+             .build())
+      .build();
+    workspaceScanner.addProjectView(workspaceRoot, projectViewSet);
+    ProjectViewVerifier.verifyProjectView(context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+    errorCollector.assertIssues(
+      "Overlapping directories: java/com/google/android/apps/example already included by java/com/google/android"
+    );
+  }
+
+  @Test
+  public void testRootDirectoryNotSpuriouslyOverlappingItself() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ListSection.builder(DirectorySection.KEY)
+                    .add(DirectoryEntry.include(new WorkspacePath("."))))
+             .build())
+      .build();
+    workspaceScanner.addProjectView(workspaceRoot, projectViewSet);
+    ProjectViewVerifier.verifyProjectView(context, workspaceRoot, projectViewSet, workspaceLanguageSettings);
+    errorCollector.assertNoIssues();
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java
new file mode 100644
index 0000000..b7a8e84
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/projectview/parser/ProjectViewParserTest.java
@@ -0,0 +1,337 @@
+/*
+ * 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.projectview.parser;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+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.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.ProjectViewStorageManager;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.DirectoryEntry;
+import com.google.idea.blaze.base.projectview.section.sections.DirectorySection;
+import com.google.idea.blaze.base.projectview.section.sections.ImportSection;
+import com.google.idea.blaze.base.projectview.section.sections.TargetSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class ProjectViewParserTest extends BlazeTestCase {
+  private ProjectViewParser projectViewParser;
+  private BlazeContext context;
+  private ErrorCollector errorCollector;
+  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/"));
+  private MockProjectViewStorageManager projectViewStorageManager;
+
+  static class MockProjectViewStorageManager extends ProjectViewStorageManager {
+    Map<String, String> projectViewFiles = Maps.newHashMap();
+    @Nullable
+    @Override
+    public String loadProjectView(@NotNull File projectViewFile) throws IOException {
+      return projectViewFiles.get(projectViewFile.getPath());
+    }
+
+    @Override
+    public void writeProjectView(@NotNull String projectViewText, @NotNull File projectViewFile) throws IOException {
+      // no-op
+    }
+
+    void add(String name, String... text) {
+      projectViewFiles.put(name, Joiner.on('\n').join(text));
+    }
+  }
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    context = new BlazeContext();
+    errorCollector = new ErrorCollector();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+    projectViewParser = new ProjectViewParser(context, new WorkspacePathResolverImpl(workspaceRoot));
+    projectViewStorageManager = new MockProjectViewStorageManager();
+    applicationServices.register(ProjectViewStorageManager.class, projectViewStorageManager);
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+    registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
+  }
+
+  @Test
+  public void testDirectoriesAndTargets() throws Exception {
+    projectViewStorageManager.add(".blazeproject",
+                                  "directories:",
+                                  "  java/com/google",
+                                  "  java/com/google/android",
+                                  "  -java/com/google/android/notme",
+                                  "",
+                                  "targets:",
+                                  "  //java/com/google:all",
+                                  "  //java/com/google/...:all",
+                                  "  -//java/com/google:thistarget"
+    );
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertNoIssues();
+
+    ProjectViewSet projectViewSet = projectViewParser.getResult();
+    ProjectViewSet.ProjectViewFile projectViewFile = projectViewSet.getTopLevelProjectViewFile();
+    assertThat(projectViewFile).isNotNull();
+    assertThat(projectViewFile.projectViewFile).isEqualTo(new File(".blazeproject"));
+    assertThat(projectViewSet.getProjectViewFiles()).containsExactly(projectViewFile);
+
+    ProjectView projectView = projectViewFile.projectView;
+    assertThat(projectView.getSectionOfType(DirectorySection.KEY).items())
+      .containsExactly(
+        new DirectoryEntry(new WorkspacePath("java/com/google"), true),
+        new DirectoryEntry(new WorkspacePath("java/com/google/android"), true),
+        new DirectoryEntry(new WorkspacePath("java/com/google/android/notme"), false)
+      );
+    assertThat(projectView.getSectionOfType(TargetSection.KEY).items())
+      .containsExactly(
+        TargetExpression.fromString("//java/com/google:all"),
+        TargetExpression.fromString("//java/com/google/...:all"),
+        TargetExpression.fromString("-//java/com/google:thistarget")
+      );
+  }
+
+  @Test
+  public void testRootDirectory() throws Exception {
+    projectViewStorageManager.add(".blazeproject",
+                                  "directories:",
+                                  "  .",
+                                  "  -java/com/google/android/notme",
+                                  "",
+                                  "targets:",
+                                  "  //java/com/google:all"
+    );
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertNoIssues();
+
+    ProjectViewSet projectViewSet = projectViewParser.getResult();
+    ProjectViewSet.ProjectViewFile projectViewFile = projectViewSet.getTopLevelProjectViewFile();
+    assertThat(projectViewFile).isNotNull();
+    assertThat(projectViewFile.projectViewFile).isEqualTo(new File(".blazeproject"));
+    assertThat(projectViewSet.getProjectViewFiles()).containsExactly(projectViewFile);
+
+    ProjectView projectView = projectViewFile.projectView;
+    assertThat(projectView.getSectionOfType(DirectorySection.KEY).items())
+      .containsExactly(
+        new DirectoryEntry(new WorkspacePath(""), true),
+        new DirectoryEntry(new WorkspacePath("java/com/google/android/notme"), false)
+      );
+    assertThat(projectView.getSectionOfType(TargetSection.KEY).items())
+      .containsExactly(
+        TargetExpression.fromString("//java/com/google:all")
+      );
+
+    String text = ProjectViewParser.projectViewToString(projectView);
+    assertThat(text).isEqualTo(
+      Joiner.on('\n').join(
+        "directories:",
+        "  .",
+        "  -java/com/google/android/notme",
+        "",
+        "targets:",
+        "  //java/com/google:all",
+        ""
+      )
+    );
+  }
+
+  @Test
+  public void testPrint() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/one")))
+             .add(DirectoryEntry.exclude(new WorkspacePath("java/com/google/two"))))
+      .put(ListSection.builder(TargetSection.KEY)
+             .add(TargetExpression.fromString("//java/com/google:one"))
+             .add(TargetExpression.fromString("//java/com/google:two")))
+      .put(ScalarSection.builder(ImportSection.KEY)
+             .set(new WorkspacePath("some/file.blazeproject")))
+      .build();
+    String text = ProjectViewParser.projectViewToString(projectView);
+    assertThat(text).isEqualTo(
+      Joiner.on('\n').join(
+        "import some/file.blazeproject",
+        "",
+        "directories:",
+        "  java/com/google/one",
+        "  -java/com/google/two",
+        "",
+        "targets:",
+        "  //java/com/google:one",
+        "  //java/com/google:two",
+        ""
+      ));
+  }
+
+  @Test
+  public void testImport() {
+    projectViewStorageManager.add("/parent.blazeproject",
+                                  "directories:",
+                                  "  parent",
+                                  "");
+    projectViewStorageManager.add(".blazeproject",
+                                  "import parent.blazeproject",
+                                  "directories:",
+                                  "  child",
+                                  "");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertNoIssues();
+
+    ProjectViewSet projectViewSet = projectViewParser.getResult();
+    assertThat(projectViewSet.getProjectViewFiles()).hasSize(2);
+    Collection<DirectoryEntry> entries = projectViewSet.listItems(DirectorySection.KEY);
+    assertThat(entries).containsExactly(
+      new DirectoryEntry(new WorkspacePath("parent"), true),
+      new DirectoryEntry(new WorkspacePath("child"), true)
+    );
+  }
+
+  @Test
+  public void testMinimumIndentRequired() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "directories:",
+                                  "  java/com/google",
+                                  "java/com/google2",
+                                  "");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertIssues("Could not parse: 'java/com/google2'");
+  }
+
+  @Test
+  public void testIncorrectIndentationResultsInIssue() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "directories:",
+                                  "  java/com/google",
+                                  " java/com/google2",
+                                  "");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertIssues("Invalid indentation. Project view files are indented with 2 spaces.");
+  }
+
+  @Test
+  public void testCanParseWithMissingCarriageReturnAtEndOfSection() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "directories:",
+                                  "  java/com/google");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    ProjectView projectView = projectViewParser.getResult().getTopLevelProjectViewFile().projectView;
+    assertThat(projectView.getSectionOfType(DirectorySection.KEY).items())
+      .containsExactly(new DirectoryEntry(new WorkspacePath("java/com/google"), true));
+  }
+
+  @Test
+  public void testWhitespaceIsIgnoredBetweenSections() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "",
+                                  "directories:",
+                                  "  java/com/google",
+                                  "",
+                                  "");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    ProjectView projectView = projectViewParser.getResult().getTopLevelProjectViewFile().projectView;
+    assertThat(projectView.getSectionOfType(DirectorySection.KEY).items())
+      .containsExactly(new DirectoryEntry(new WorkspacePath("java/com/google"), true));
+  }
+
+  @Test
+  public void testImportMissingFileResultsInIssue() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "import parent.blazeproject");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertIssues("Could not load project view file: '/parent.blazeproject'");
+  }
+
+  @Test
+  public void testDuplicateSectionsResultsInIssue() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "directories:",
+                                  "  java/com/google",
+                                  "directories:",
+                                  "  java/com/google");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertIssues("Duplicate attribute: 'directories'");
+  }
+
+  @Test
+  public void testMissingSectionResultsInIssue() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "nosuchsection:",
+                                  "  java/com/google");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertIssues("Could not parse: 'nosuchsection:'");
+  }
+
+  @Test
+  public void testMissingColonResultInIssue() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "directories",
+                                  "  java/com/google");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertIssues("Could not parse: 'directories'");
+  }
+
+  @Test
+  public void testEmptySectionYieldsError() {
+    projectViewStorageManager.add(".blazeproject",
+                                  "directories:",
+                                  "");
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertIssues("Empty section: 'directories'");
+  }
+
+  @Test
+  public void testCommentsAreSkipped() throws Exception {
+    projectViewStorageManager.add(".blazeproject",
+                                  "# comment",
+                                  "directories:",
+                                  "# another comment",
+                                  "  java/com/google",
+                                  "  # comment",
+                                  "  java/com/google/android",
+                                  ""
+    );
+    projectViewParser.parseProjectView(new File(".blazeproject"));
+    errorCollector.assertNoIssues();
+
+    ProjectViewSet projectViewSet = projectViewParser.getResult();
+    ProjectViewSet.ProjectViewFile projectViewFile = projectViewSet.getTopLevelProjectViewFile();
+    ProjectView projectView = projectViewFile.projectView;
+    assertThat(projectView.getSectionOfType(DirectorySection.KEY).items())
+      .containsExactly(
+        new DirectoryEntry(new WorkspacePath("java/com/google"), true),
+        new DirectoryEntry(new WorkspacePath("java/com/google/android"), true)
+      );
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/rulemaps/ReverseDependencyMapTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/rulemaps/ReverseDependencyMapTest.java
new file mode 100644
index 0000000..7a3a524
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/rulemaps/ReverseDependencyMapTest.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.rulemaps;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class ReverseDependencyMapTest extends BlazeTestCase {
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+  }
+
+  @Test
+  public void testSingleDep() {
+    RuleMapBuilder builder = RuleMapBuilder.builder();
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = builder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l1")
+                 .setKind("java_library")
+                 .addDependency("//l:l2"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l2")
+                 .setKind("java_library"))
+      .build();
+
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l2"), new Label("//l:l1"));
+  }
+
+  @Test
+  public void testLabelDepsOnTwoLabels() {
+    RuleMapBuilder builder = RuleMapBuilder.builder();
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = builder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l1")
+                 .setKind("java_library")
+                 .addDependency("//l:l2")
+                 .addDependency("//l:l3"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l2")
+                 .setKind("java_library"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l3")
+                 .setKind("java_library"))
+      .build();
+
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l2"), new Label("//l:l1"));
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l1"));
+  }
+
+  @Test
+  public void testTwoLabelsDepOnSameLabel() {
+    RuleMapBuilder builder = RuleMapBuilder.builder();
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = builder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l1")
+                 .setKind("java_library")
+                 .addDependency("//l:l3"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l2")
+                 .addDependency("//l:l3")
+                 .setKind("java_library"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l3")
+                 .setKind("java_library"))
+      .build();
+
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l1"));
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l2"));
+  }
+
+  @Test
+  public void testThreeLevelGraph() {
+    RuleMapBuilder builder = RuleMapBuilder.builder();
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = builder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l1")
+                 .setKind("java_library")
+                 .addDependency("//l:l3"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l2")
+                 .addDependency("//l:l3")
+                 .setKind("java_library"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l3")
+                 .setKind("java_library"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l4")
+                 .addDependency("//l:l3")
+                 .setKind("java_library"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//l:l5")
+                 .addDependency("//l:l4")
+                 .setKind("java_library"))
+      .build();
+
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l1"));
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l2"));
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l3"), new Label("//l:l4"));
+    assertThat(reverseDependencies).containsEntry(new Label("//l:l4"), new Label("//l:l5"));
+  }
+
+  private static ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder()
+      .setRootPath("/")
+      .setRelativePath(relativePath)
+      .setIsSource(true)
+      .build();
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java
new file mode 100644
index 0000000..2930623
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/run/testmap/TestMapTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.run.testmap;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.rulemaps.ReverseDependencyMap;
+import com.google.idea.blaze.base.run.testmap.TestRuleFinderImpl.TestMap;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.File;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class TestMapTest extends BlazeTestCase {
+  private RuleMapBuilder ruleMapBuilder;
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+    ruleMapBuilder = RuleMapBuilder.builder();
+  }
+
+  @Test
+  public void testTrivialTestMap() throws Exception {
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = ruleMapBuilder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test")
+                 .setKind("java_test")
+                 .addSource(sourceRoot("test/Test.java")))
+      .build();
+
+    TestMap testMap = new TestMap(project, ruleMap);
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java"), null))
+      .containsExactly(new Label("//test:test"));
+  }
+
+  @Test
+  public void testOneStepRemovedTestMap() throws Exception {
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = ruleMapBuilder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test")
+                 .setKind("java_test")
+                 .addDependency("//test:lib"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("test/Test.java")))
+      .build();
+
+    TestMap testMap = new TestMap(project, ruleMap);
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java"), null))
+      .containsExactly(new Label("//test:test"));
+  }
+
+  @Test
+  public void testTwoCandidatesTestMap() throws Exception {
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = ruleMapBuilder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test")
+                 .setKind("java_test")
+                 .addDependency("//test:lib"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test2")
+                 .setKind("java_test")
+                 .addDependency("//test:lib"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("test/Test.java")))
+      .build();
+
+    TestMap testMap = new TestMap(project, ruleMap);
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java"), null))
+      .containsExactly(new Label("//test:test"), new Label("//test:test2"));
+  }
+
+  @Test
+  public void testBfsPreferred() throws Exception {
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = ruleMapBuilder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("test/Test.java")))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib2")
+                 .setKind("java_library")
+                 .addDependency("//test:lib"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test2")
+                 .setKind("java_test")
+                 .addDependency("//test:lib2"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test")
+                 .setKind("java_test")
+                 .addDependency("//test:lib"))
+      .build();
+
+    TestMap testMap = new TestMap(project, ruleMap);
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java"), null))
+      .containsExactly(new Label("//test:test"), new Label("//test:test2"))
+      .inOrder();
+  }
+
+  @Test
+  public void testTestSizeFilter() throws Exception {
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = ruleMapBuilder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("test/Test.java")))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib2")
+                 .setKind("java_library")
+                 .addDependency("//test:lib"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test2")
+                 .setKind("java_test")
+                 .setTestInfo(TestIdeInfo.builder()
+                       .setTestSize(TestIdeInfo.TestSize.LARGE))
+                 .addDependency("//test:lib2"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test")
+                 .setKind("java_test")
+                 .setTestInfo(TestIdeInfo.builder())
+                 .addDependency("//test:lib"))
+      .build();
+
+    TestMap testMap = new TestMap(project, ruleMap);
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java"), TestIdeInfo.TestSize.LARGE))
+      .containsExactly(new Label("//test:test2"))
+      .inOrder();
+  }
+
+  @Test
+  public void testSourceIncludedMultipleTimesFindsAll() throws Exception {
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = ruleMapBuilder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test")
+                 .setKind("java_test")
+                 .addDependency("//test:lib"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test2")
+                 .setKind("java_test")
+                 .addDependency("//test:lib2"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("test/Test.java")))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib2")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("test/Test.java")))
+      .build();
+
+    TestMap testMap = new TestMap(project, ruleMap);
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java"), null))
+      .containsExactly(new Label("//test:test"), new Label("//test:test2"));
+  }
+
+  @Test
+  public void testSourceIncludedMultipleTimesShouldOnlyGiveOneInstanceOfTest() throws Exception {
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = ruleMapBuilder
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:test")
+                 .setKind("java_test")
+                 .addDependency("//test:lib")
+                 .addDependency("//test:lib2"))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("test/Test.java")))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("test/BUILD"))
+                 .setLabel("//test:lib2")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("test/Test.java")))
+      .build();
+
+    TestMap testMap = new TestMap(project, ruleMap);
+    ImmutableMultimap<Label, Label> reverseDependencies = ReverseDependencyMap.createRdepsMap(ruleMap);
+    assertThat(testMap.testTargetsForSourceFile(reverseDependencies, new File("/test/Test.java"), null))
+      .containsExactly(new Label("//test:test"));
+  }
+
+  private ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder()
+      .setRootPath("/")
+      .setRelativePath(relativePath)
+      .setIsSource(true)
+      .build();
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/scope/BlazeContextTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/scope/BlazeContextTest.java
new file mode 100644
index 0000000..0b0f2a5
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/scope/BlazeContextTest.java
@@ -0,0 +1,390 @@
+/*
+ * 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.scope;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.BlazeTestCase;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link BlazeContext}.
+ */
+public class BlazeContextTest extends BlazeTestCase {
+
+  @Test
+  public void testScopeBeginsWhenPushedToContext() {
+    BlazeContext context = new BlazeContext();
+    final BlazeScope scope = mock(BlazeScope.class);
+    context.push(scope);
+    verify(scope).onScopeBegin(context);
+  }
+
+  @Test
+  public void testScopeEndsWhenContextEnds() {
+    BlazeContext context = new BlazeContext();
+    final BlazeScope scope = mock(BlazeScope.class);
+    context.push(scope);
+    context.endScope();
+    verify(scope).onScopeEnd(context);
+  }
+
+  @Test
+  public void testEndingTwiceHasNoEffect() {
+    BlazeContext context = new BlazeContext();
+    final BlazeScope scope = mock(BlazeScope.class);
+    context.push(scope);
+    context.endScope();
+    context.endScope();
+    verify(scope).onScopeEnd(context);
+  }
+
+  @Test
+  public void testEndingScopeNormallyDoesntEndParent() {
+    BlazeContext parentContext = new BlazeContext();
+    BlazeContext childContext = new BlazeContext(parentContext);
+    childContext.endScope();
+    assertTrue(childContext.isEnding());
+    assertFalse(parentContext.isEnding());
+  }
+
+  @Test
+  public void testCancellingScopeCancelsParent() {
+    BlazeContext parentContext = new BlazeContext();
+    BlazeContext childContext = new BlazeContext(parentContext);
+    childContext.setCancelled();
+    assertTrue(childContext.isCancelled());
+    assertTrue(parentContext.isCancelled());
+  }
+
+  /**
+   * A simple scope that records its start and end by ID.
+   */
+  static class RecordScope implements BlazeScope {
+    private final int id;
+    private final List<String> record;
+
+    public RecordScope(int id, List<String> record) {
+      this.id = id;
+      this.record = record;
+    }
+
+    @Override
+    public void onScopeBegin(@NotNull BlazeContext context) {
+      record.add("begin" + id);
+    }
+
+    @Override
+    public void onScopeEnd(@NotNull BlazeContext context) {
+      record.add("end" + id);
+    }
+  }
+
+  @Test
+  public void testScopesBeginAndEndInStackOrder() {
+    List<String> record = Lists.newArrayList();
+    BlazeContext context = new BlazeContext();
+    context
+      .push(new RecordScope(1, record))
+      .push(new RecordScope(2, record))
+      .push(new RecordScope(3, record));
+    context.endScope();
+    assertThat(record)
+      .isEqualTo(ImmutableList.of("begin1", "begin2", "begin3", "end3", "end2", "end1"));
+  }
+
+  @Test
+  public void testParentFoundInStackOrder() {
+    BlazeContext context = new BlazeContext();
+    BlazeScope scope1 = mock(BlazeScope.class);
+    BlazeScope scope2 = mock(BlazeScope.class);
+    BlazeScope scope3 = mock(BlazeScope.class);
+    context
+      .push(scope1)
+      .push(scope2)
+      .push(scope3);
+    assertThat(context.getParentScope(scope3)).isEqualTo(scope2);
+    assertThat(context.getParentScope(scope2)).isEqualTo(scope1);
+    assertThat(context.getParentScope(scope1)).isNull();
+  }
+
+  @Test
+  public void testParentFoundInStackOrderAcrossContexts() {
+    BlazeContext parentContext = new BlazeContext();
+    BlazeContext childContext = new BlazeContext(parentContext);
+    BlazeScope scope1 = mock(BlazeScope.class);
+    BlazeScope scope2 = mock(BlazeScope.class);
+    BlazeScope scope3 = mock(BlazeScope.class);
+    parentContext
+      .push(scope1)
+      .push(scope2);
+    childContext
+      .push(scope3);
+    assertThat(childContext.getParentScope(scope3)).isEqualTo(scope2);
+  }
+
+  static class TestOutput1 implements Output {
+  }
+
+  static class TestOutput2 implements Output {
+  }
+
+  static class TestOutputSink<T extends Output> implements OutputSink<T> {
+    public boolean gotOutput;
+
+    @Override
+    public Propagation onOutput(@NotNull T output) {
+      gotOutput = true;
+      return Propagation.Continue;
+    }
+  }
+
+  static class TestOutputSink1 extends TestOutputSink<TestOutput1> {
+  }
+
+  static class TestOutputSink2 extends TestOutputSink<TestOutput2> {
+  }
+
+  @Test
+  public void testOutputGoesToRegisteredSink() {
+    BlazeContext context = new BlazeContext();
+    TestOutputSink1 sink = new TestOutputSink1();
+    context.addOutputSink(TestOutput1.class, sink);
+
+    assertFalse(sink.gotOutput);
+    context.output(new TestOutput1());
+    assertTrue(sink.gotOutput);
+  }
+
+  @Test
+  public void testOutputDoesntGoToWrongSink() {
+    BlazeContext context = new BlazeContext();
+    TestOutputSink2 sink = new TestOutputSink2();
+    context.addOutputSink(TestOutput2.class, sink);
+
+    assertFalse(sink.gotOutput);
+    context.output(new TestOutput1());
+    assertFalse(sink.gotOutput);
+  }
+
+  @Test
+  public void testOutputGoesToParentContexts() {
+    BlazeContext parentContext = new BlazeContext();
+    BlazeContext childContext = new BlazeContext(parentContext);
+    TestOutputSink1 sink = new TestOutputSink1();
+    parentContext.addOutputSink(TestOutput1.class, sink);
+
+    assertFalse(sink.gotOutput);
+    childContext.output(new TestOutput1());
+    assertTrue(sink.gotOutput);
+  }
+
+  @Test
+  public void testHoldingPreventsEndingContext() {
+    BlazeContext context = new BlazeContext();
+    context.hold();
+    context.endScope();
+    assertFalse(context.isEnding());
+    context.release();
+    assertTrue(context.isEnding());
+  }
+
+  private static class StringScope implements BlazeScope {
+
+    public final String str;
+
+    public StringScope(String s) {
+      this.str = s;
+    }
+
+    @Override
+    public void onScopeBegin(@NotNull BlazeContext context) {
+
+    }
+
+    @Override
+    public void onScopeEnd(@NotNull BlazeContext context) {
+
+    }
+  }
+
+  private static class CollectorScope implements BlazeScope {
+
+    public final List<String> output;
+
+    public CollectorScope(List<String> output) {
+      this.output = output;
+    }
+
+    @Override
+    public void onScopeBegin(@NotNull BlazeContext context) {
+
+    }
+
+    @Override
+    public void onScopeEnd(@NotNull BlazeContext context) {
+      List<StringScope> scopes = context.getScopes(StringScope.class, this);
+      for (StringScope scope : scopes) {
+        output.add(scope.str);
+      }
+    }
+  }
+
+  @Test
+  public void testGetScopesOnlyReturnsScopesLowerOnTheStack() {
+    List<String> output1 = Lists.newArrayList();
+    List<String> output2 = Lists.newArrayList();
+    List<String> output3 = Lists.newArrayList();
+
+    BlazeContext context = new BlazeContext();
+    context.push(new StringScope("a"));
+    context.push(new StringScope("b"));
+    CollectorScope scope = new CollectorScope(output1);
+    context.push(scope);
+    context.push(new StringScope("c"));
+    context.push(new CollectorScope(output2));
+    context.push(new StringScope("d"));
+    context.push(new StringScope("e"));
+    context.push(new CollectorScope(output3));
+    context.endScope();
+
+    assertThat(output1).isEqualTo(ImmutableList.of("b", "a"));
+    assertThat(output2).isEqualTo(ImmutableList.of("c", "b", "a"));
+    assertThat(output3).isEqualTo(ImmutableList.of("e", "d", "c", "b", "a"));
+  }
+
+  @Test
+  public void testGetScopesOnlyReturnsScopesLowerOnTheStackForMultipleContexts() {
+    List<String> output1 = Lists.newArrayList();
+    List<String> output2 = Lists.newArrayList();
+    List<String> output3 = Lists.newArrayList();
+
+    BlazeContext context1 = new BlazeContext();
+    context1.push(new StringScope("a"));
+    context1.push(new StringScope("b"));
+    CollectorScope scope = new CollectorScope(output1);
+    context1.push(scope);
+
+    BlazeContext context2 = new BlazeContext(context1);
+    context2.push(new StringScope("c"));
+    context2.push(new CollectorScope(output2));
+    context2.push(new StringScope("d"));
+    context2.push(new StringScope("e"));
+
+    BlazeContext context3 = new BlazeContext(context2);
+    context3.push(new CollectorScope(output3));
+    context3.endScope();
+    context2.endScope();
+    context1.endScope();
+
+    assertThat(output1).isEqualTo(ImmutableList.of("b", "a"));
+    assertThat(output2).isEqualTo(ImmutableList.of("c", "b", "a"));
+    assertThat(output3).isEqualTo(ImmutableList.of("e", "d", "c", "b", "a"));
+  }
+
+  @Test
+  public void testGetScopesOnlyReturnsScopesIfStartingScopeInContext() {
+    List<String> output1 = Lists.newArrayList();
+
+    BlazeContext context1 = new BlazeContext();
+    context1.push(new StringScope("a"));
+    context1.push(new StringScope("b"));
+    CollectorScope scope = new CollectorScope(output1);
+    context1.push(scope);
+
+    BlazeContext context2 = new BlazeContext(context1);
+    context2.push(new StringScope("c"));
+
+    List<StringScope> scopes = context2.getScopes(StringScope.class, scope);
+    assertThat(scopes).isEqualTo(ImmutableList.of());
+  }
+
+  @Test
+  public void testGetScopesIncludesStartingScope() {
+    BlazeContext context1 = new BlazeContext();
+    StringScope a = new StringScope("a");
+    context1.push(a);
+    StringScope b = new StringScope("b");
+    context1.push(b);
+
+    List<StringScope> scopes = context1.getScopes(StringScope.class, b);
+    assertThat(scopes).isEqualTo(ImmutableList.of(b, a));
+  }
+
+  @Test
+  public void testGetScopesIndexIsNoninclusive() {
+    BlazeContext context1 = new BlazeContext();
+    StringScope scopeA = new StringScope("a");
+    context1.push(scopeA);
+    StringScope scopeB = new StringScope("b");
+    context1.push(scopeB);
+
+    List<StringScope> scopes = Lists.newArrayList();
+    context1.getScopes(scopes, StringScope.class, 1);
+    assertThat(scopes).isEqualTo(ImmutableList.of(scopeA));
+  }
+
+  @Test
+  public void testGetScopesWithoutStartScopeGetsAll() {
+    BlazeContext context1 = new BlazeContext();
+    StringScope a = new StringScope("a");
+    context1.push(a);
+    StringScope b = new StringScope("b");
+    context1.push(b);
+
+    List<StringScope> scopes = context1.getScopes(StringScope.class);
+    assertThat(scopes).isEqualTo(ImmutableList.of(b, a));
+  }
+
+  static class NonPropagatingOutputSink implements OutputSink<TestOutput1> {
+    boolean gotOutput;
+
+    @Override
+    public Propagation onOutput(@NotNull TestOutput1 output) {
+      this.gotOutput = true;
+      return Propagation.Stop;
+    }
+  }
+
+  @Test
+  public void testOutputIsTerminatedByFirstSink() {
+    NonPropagatingOutputSink sink1 = new NonPropagatingOutputSink();
+    NonPropagatingOutputSink sink2 = new NonPropagatingOutputSink();
+    NonPropagatingOutputSink sink3 = new NonPropagatingOutputSink();
+
+    BlazeContext context1 = new BlazeContext();
+    context1.addOutputSink(TestOutput1.class, sink1);
+
+    BlazeContext context2 = new BlazeContext(context1);
+    context2.addOutputSink(TestOutput1.class, sink2);
+    context2.addOutputSink(TestOutput1.class, sink3);
+
+    context2.output(new TestOutput1());
+
+    assertThat(sink1.gotOutput).isFalse();
+    assertThat(sink2.gotOutput).isFalse();
+    assertThat(sink3.gotOutput).isTrue();
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/scope/ScopeTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/scope/ScopeTest.java
new file mode 100644
index 0000000..e9db247
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/scope/ScopeTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.scope;
+
+import com.google.idea.blaze.base.BlazeTestCase;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests for {@link Scope}.
+ */
+public class ScopeTest extends BlazeTestCase {
+
+  @Test
+  public void testScopedOperationRuns() {
+    final boolean[] ran = new boolean[1];
+    Scope.root(new ScopedOperation() {
+      @Override
+      public void execute(@NotNull BlazeContext context) {
+        ran[0] = true;
+      }
+    });
+    assertTrue(ran[0]);
+  }
+
+  @Test
+  public void testScopedFunctionReturnsValue() {
+    String result = Scope.root(new ScopedFunction<String>() {
+      @Override
+      public String execute(@NotNull BlazeContext context) {
+        return "test";
+      }
+    });
+    assertThat(result).isEqualTo("test");
+  }
+
+  @Test
+  public void testScopedOperationEndsContext() {
+    final BlazeScope scope = mock(BlazeScope.class);
+    Scope.root(new ScopedOperation() {
+      @Override
+      public void execute(@NotNull BlazeContext context) {
+        context.push(scope);
+      }
+    });
+    verify(scope).onScopeEnd(any(BlazeContext.class));
+  }
+
+  @Test
+  public void testScopedFunctionEndsContext() {
+    final BlazeScope scope = mock(BlazeScope.class);
+    Scope.root(new ScopedFunction<String>() {
+      @Override
+      public String execute(@NotNull BlazeContext context) {
+        context.push(scope);
+        return "";
+      }
+    });
+    verify(scope).onScopeEnd(any(BlazeContext.class));
+  }
+
+  /*
+  @Test
+  public void testThrowingExceptionEndsScopedOperationWithFailure() {
+    final RuntimeException e = new RuntimeException();
+    final BlazeScope scope = mock(BlazeScope.class);
+    Throwable throwable = Scope.root(project, new ScopedOperation() {
+      @Override
+      public void execute(@NotNull BlazeContext context) {
+        context.push(scope);
+        throw e;
+      }
+    }).throwable;
+    verify(scope).onScopeEnd(any(BlazeContext.class));
+    assertThat(e).isEqualTo(throwable);
+  }
+
+  @Test
+  public void testThrowingExceptionEndsScopeFunctionWithFailure() {
+    final RuntimeException e = new RuntimeException();
+    final BlazeScope scope = mock(BlazeScope.class);
+    Throwable throwable = Scope.root(project, new ScopedFunction<String>() {
+      @Override
+      public String execute(@NotNull BlazeContext context) {
+        context.push(scope);
+        throw e;
+      }
+    }).throwable;
+    verify(scope).onScopeEnd(any(BlazeContext.class));
+    assertThat(e).isEqualTo(throwable);
+  }
+  */
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.java
new file mode 100644
index 0000000..7318c00
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/LanguageSupportTest.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.base.sync;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.sync.projectview.LanguageSupport;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.ScalarSection;
+import com.google.idea.blaze.base.projectview.section.sections.WorkspaceTypeSection;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.util.Set;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Test cases for {@link LanguageSupport}
+ */
+public class LanguageSupportTest extends BlazeTestCase {
+  private ErrorCollector errorCollector = new ErrorCollector();
+  private BlazeContext context;
+  private ExtensionPointImpl<BlazeSyncPlugin> syncPlugins;
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    syncPlugins = registerExtensionPoint(BlazeSyncPlugin.EP_NAME, BlazeSyncPlugin.class);
+
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+  }
+
+  @Test
+  public void testSimpleCase() {
+    syncPlugins.registerExtension(new BlazeSyncPlugin.Adapter() {
+      @Override
+      public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+        return ImmutableSet.of(LanguageClass.C);
+      }
+    });
+
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ScalarSection.builder(WorkspaceTypeSection.KEY)
+                    .set(WorkspaceType.C))
+             .build())
+      .build();
+    WorkspaceLanguageSettings workspaceLanguageSettings = LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    errorCollector.assertNoIssues();
+    assertThat(workspaceLanguageSettings).isEqualTo(
+      new WorkspaceLanguageSettings(WorkspaceType.C, ImmutableSet.of(LanguageClass.C))
+    );
+  }
+
+  @Test
+  public void testFailWithUnsupportedLanguage() {
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ScalarSection.builder(WorkspaceTypeSection.KEY)
+                    .set(WorkspaceType.C))
+             .build())
+      .build();
+    LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    errorCollector.assertIssues(
+      "Language 'c' is not supported for this plugin with workspace type: 'c'");
+  }
+
+  /**
+   * Tests that we ask for java and android when the workspace type is android.
+   */
+  @Test
+  public void testWorkspaceTypeImpliesLanguages() {
+    syncPlugins.registerExtension(new BlazeSyncPlugin.Adapter() {
+      @Override
+      public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+        return ImmutableSet.of(LanguageClass.ANDROID, LanguageClass.JAVA, LanguageClass.C);
+      }
+    });
+
+    ProjectViewSet projectViewSet = ProjectViewSet.builder()
+      .add(ProjectView.builder()
+             .put(ScalarSection.builder(WorkspaceTypeSection.KEY)
+                    .set(WorkspaceType.ANDROID))
+             .build())
+      .build();
+    WorkspaceLanguageSettings workspaceLanguageSettings = LanguageSupport.createWorkspaceLanguageSettings(context, projectViewSet);
+    assertThat(workspaceLanguageSettings).isEqualTo(
+      new WorkspaceLanguageSettings(WorkspaceType.ANDROID, ImmutableSet.of(LanguageClass.JAVA, LanguageClass.ANDROID))
+    );
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java
new file mode 100644
index 0000000..0f9b711
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/aspects/BlazeIdeInterfaceAspectsImplTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.aspects;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.TestUtils;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.*;
+import com.google.idea.blaze.base.sync.filediff.FileDiffService;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.File;
+
+/**
+ * Tests for {@link BlazeIdeInterfaceAspectsImpl}.
+ */
+public class BlazeIdeInterfaceAspectsImplTest extends BlazeTestCase {
+
+  private static final File DUMMY_ROOT = new File("/");
+  private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(DUMMY_ROOT);
+  private static final BlazeRoots BLAZE_ROOTS = new BlazeRoots(
+    DUMMY_ROOT,
+    ImmutableList.of(DUMMY_ROOT),
+    new ExecutionRootPath("out/crosstool/bin"),
+    new ExecutionRootPath("out/crosstool/gen")
+  );
+  private static final ArtifactLocationDecoder DUMMY_DECODER = new ArtifactLocationDecoder(
+    BLAZE_ROOTS,
+    new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_ROOTS)
+  );
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices,
+                          @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+    applicationServices.register(FileAttributeProvider.class, new FileAttributeProvider());
+  }
+
+  @Test
+  public void testRuleIdeInfoIsSerializable() {
+    AndroidStudioIdeInfo.RuleIdeInfo ideProto = AndroidStudioIdeInfo.RuleIdeInfo.newBuilder()
+      .setLabel("//test:test")
+      .setBuildFile("build")
+      .setKindString("android_binary")
+      .addDependencies("//test:dep")
+      .addTags("tag")
+      .setJavaRuleIdeInfo(AndroidStudioIdeInfo.JavaRuleIdeInfo.newBuilder()
+                            .addJars(AndroidStudioIdeInfo.LibraryArtifact.newBuilder()
+                                       .setJar(artifactLocation("jar.jar")).build())
+                            .addGeneratedJars(AndroidStudioIdeInfo.LibraryArtifact.newBuilder()
+                                                .setJar(artifactLocation("jar.jar")).build())
+                            .addSources(artifactLocation("source.java")))
+      .setAndroidRuleIdeInfo(AndroidStudioIdeInfo.AndroidRuleIdeInfo.newBuilder()
+                             .addResources(artifactLocation("res"))
+                             .setApk(artifactLocation("apk"))
+                             .addDependencyApk(artifactLocation("apk"))
+                             .setJavaPackage("package"))
+      .build();
+
+    WorkspaceLanguageSettings workspaceLanguageSettings = new WorkspaceLanguageSettings(WorkspaceType.ANDROID,
+                                                                                        ImmutableSet.of(LanguageClass.ANDROID));
+    RuleIdeInfo ruleIdeInfo = IdeInfoFromProtobuf.makeRuleIdeInfo(workspaceLanguageSettings, DUMMY_DECODER, ideProto);
+    TestUtils.assertIsSerializable(ruleIdeInfo);
+  }
+
+  @Test
+  public void testBlazeStateIsSerializable() {
+    BlazeIdeInterfaceAspectsImpl.State state = new BlazeIdeInterfaceAspectsImpl.State();
+    state.fileToLabel = ImmutableMap.of(new File("fileName"), new Label("//java/com/test:test"));
+    state.fileState = new FileDiffService.State();
+    state.androidPlatformDirectory = new File("");
+    state.androidPlatformDirectory  = new File("dir");
+    state.ruleMap = ImmutableMap.of(); // Tested separately in testRuleIdeInfoIsSerializable
+
+    TestUtils.assertIsSerializable(state);
+  }
+
+
+  static AndroidStudioIdeInfo.ArtifactLocation artifactLocation(String relativePath) {
+    return artifactLocation(DUMMY_ROOT.toString(), relativePath);
+  }
+
+  static AndroidStudioIdeInfo.ArtifactLocation artifactLocation(String rootPath, String relativePath) {
+    return AndroidStudioIdeInfo.ArtifactLocation.newBuilder().setRootPath(rootPath).setRelativePath(relativePath).build();
+  }
+
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptionsTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptionsTest.java
new file mode 100644
index 0000000..e95f0eb
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/aspects/UnfilteredCompilerOptionsTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.aspects;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.BlazeTestCase;
+import org.junit.Test;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class UnfilteredCompilerOptionsTest extends BlazeTestCase {
+  @Test
+  public void testUnfilteredOptionsParsingForISystemOptions() {
+    ImmutableList<String> unfilteredOptions = ImmutableList.of(
+      "-isystem",
+      "sys/inc1",
+      "-VER2",
+      "-isystem",
+      "sys2/inc1",
+      "-isystem",
+      "sys3/inc1",
+      "-isystm",
+      "sys4/inc1"
+    );
+    List<String> sysIncludes = Lists.newArrayList();
+    List<String> flags = Lists.newArrayList();
+    UnfilteredCompilerOptions.splitUnfilteredCompilerOptions(unfilteredOptions, sysIncludes, flags);
+
+    assertThat(sysIncludes).containsExactly(
+      "sys/inc1",
+      "sys2/inc1",
+      "sys3/inc1"
+    );
+
+    assertThat(flags).containsExactly(
+      "-VER2",
+      "-isystm",
+      "sys4/inc1"
+    );
+  }
+
+  @Test
+  public void testUnfilteredOptionsParsingForISystemOptionsNoSpaceAfterIsystem() {
+    ImmutableList<String> unfilteredOptions = ImmutableList.of(
+      "-isystem",
+      "sys/inc1",
+      "-VER2",
+      "-isystemsys2/inc1",
+      "-isystem",
+      "sys3/inc1"
+    );
+    List<String> sysIncludes = Lists.newArrayList();
+    List<String> flags = Lists.newArrayList();
+    UnfilteredCompilerOptions.splitUnfilteredCompilerOptions(unfilteredOptions, sysIncludes, flags);
+
+    assertThat(sysIncludes).containsExactly(
+      "sys/inc1",
+      "sys2/inc1",
+      "sys3/inc1"
+    );
+
+    assertThat(flags).containsExactly("-VER2");
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/filediff/FileDiffServiceTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/filediff/FileDiffServiceTest.java
new file mode 100644
index 0000000..b5d107e
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/filediff/FileDiffServiceTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.filediff;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.executor.MockBlazeExecutor;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for FileDiffService
+ */
+public class FileDiffServiceTest extends BlazeTestCase {
+  private MockFileAttributeProvider fileModificationProvider;
+  private FileDiffService fileDiffService;
+
+  private static class MockFileAttributeProvider extends FileAttributeProvider {
+    List<Long> times = Lists.newArrayList();
+    int index;
+
+    public MockFileAttributeProvider add(long time) {
+      times.add(time);
+      return this;
+    }
+
+    @Override
+    public long getFileModifiedTime(@NotNull File file) {
+      return times.get(index++);
+    }
+  }
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices,
+                          @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+    applicationServices.register(BlazeExecutor.class, new MockBlazeExecutor());
+
+    this.fileModificationProvider = new MockFileAttributeProvider();
+    applicationServices.register(FileAttributeProvider.class, fileModificationProvider);
+    this.fileDiffService = new FileDiffService();
+  }
+
+  @Test
+  public void testDiffWithDiffMethodTimestamp() throws Exception {
+    Map<File, FileDiffService.FileEntry> oldFiles = fileMap(
+      fileEntry("file1", 13),
+      fileEntry("file2", 17),
+      fileEntry("file3", 21)
+    );
+    FileDiffService.State oldState = new FileDiffService.State();
+    oldState.fileEntryMap = oldFiles;
+    List<File> fileList = ImmutableList.of(new File("file1"), new File("file2"));
+    fileModificationProvider.add(13).add(122);
+
+    List<File> newFiles = Lists.newArrayList();
+    List<File> removedFiles = Lists.newArrayList();
+    fileDiffService.updateFiles(
+      oldState,
+      fileList,
+      newFiles,
+      removedFiles
+    );
+
+    assertThat(newFiles).containsExactly(new File("file2"));
+    assertThat(removedFiles).containsExactly(new File("file3"));
+  }
+
+  static Map<File, FileDiffService.FileEntry> fileMap(FileDiffService.FileEntry... fileEntries) {
+    ImmutableMap.Builder<File, FileDiffService.FileEntry> builder = ImmutableMap.builder();
+    for (FileDiffService.FileEntry fileEntry : fileEntries) {
+      builder.put(fileEntry.file, fileEntry);
+    }
+    return builder.build();
+  }
+
+  static FileDiffService.FileEntry fileEntry(@NotNull String filePath, long timestamp) {
+    FileDiffService.FileEntry fileEntry = new FileDiffService.FileEntry();
+    fileEntry.file = new File(filePath);
+    fileEntry.timestamp = timestamp;
+    return fileEntry;
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
new file mode 100644
index 0000000..3440275
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/workspace/ArtifactLocationDecoderTest.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.sync.workspace;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.AndroidStudioIdeInfo;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.util.List;
+import java.util.Set;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Test cases for {@link ArtifactLocationDecoder}.
+ */
+@RunWith(JUnit4.class)
+public class ArtifactLocationDecoderTest extends BlazeTestCase {
+
+  private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(new File("/path/to/root"));
+  private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
+
+  private static final BlazeRoots BLAZE_GIT5_ROOTS = new BlazeRoots(
+    new File(EXECUTION_ROOT),
+    ImmutableList.of(
+      WORKSPACE_ROOT.directory(),
+      new File(WORKSPACE_ROOT.directory().getParentFile(), "READONLY/root")
+    ),
+    new ExecutionRootPath("root/blaze-out/crosstool/bin"),
+    new ExecutionRootPath("root/blaze-out/crosstool/genfiles")
+  );
+
+  private static final BlazeRoots BLAZE_CITC_ROOTS = new BlazeRoots(
+    new File(EXECUTION_ROOT),
+    ImmutableList.of(WORKSPACE_ROOT.directory()),
+    new ExecutionRootPath("root/blaze-out/crosstool/bin"),
+    new ExecutionRootPath("root/blaze-out/crosstool/genfiles")
+  );
+
+  static class MockFileAttributeProvider extends FileAttributeProvider {
+    final Set<File> files = Sets.newHashSet();
+
+    void addFiles(@NotNull File... files) {
+      this.files.addAll(Lists.newArrayList(files));
+    }
+
+    @Override
+    public boolean exists(@NotNull File file) {
+      return files.contains(file);
+    }
+  }
+
+  private MockFileAttributeProvider fileChecker;
+
+  @Override
+  protected void initTest(
+    @NotNull Container applicationServices,
+    @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    fileChecker = new MockFileAttributeProvider();
+    applicationServices.register(
+      FileAttributeProvider.class,
+      fileChecker
+    );
+  }
+
+  @Test
+  public void testManualPackagePaths() throws Exception {
+    List<File> packagePaths = ImmutableList.of(
+      WORKSPACE_ROOT.directory(),
+      new File(WORKSPACE_ROOT.directory().getParentFile(), "READONLY/root"),
+      new File(WORKSPACE_ROOT.directory().getParentFile(), "CUSTOM/root")
+    );
+
+    BlazeRoots BLAZE_ROOTS = new BlazeRoots(
+      new File(EXECUTION_ROOT),
+      packagePaths,
+      new ExecutionRootPath("root/blaze-out/crosstool/bin"),
+      new ExecutionRootPath("root/blaze-out/crosstool/genfiles")
+    );
+
+    fileChecker.addFiles(new File(packagePaths.get(0), "com/google/Bla.java"),
+                         new File(packagePaths.get(1), "com/google/Foo.java"),
+                         new File(packagePaths.get(2), "com/other/Test.java"));
+
+    ArtifactLocationDecoder decoder = new ArtifactLocationDecoder(BLAZE_ROOTS, new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_ROOTS));
+
+    ArtifactLocationBuilder builder = new ArtifactLocationBuilder()
+      .setRootPath("UNUSED_DUMMY")
+      .setRelativePath("com/google/Bla.java")
+      .setIsSource(true)
+      .setVersion(Version.Current);
+
+    assertThat(decoder.decode(builder.buildIdeInfoArtifact()).getRootPath())
+      .isEqualTo(packagePaths.get(0).toString());
+
+    builder.setRelativePath("com/google/Foo.java");
+
+    assertThat(decoder.decode(builder.buildIdeInfoArtifact()).getRootPath())
+      .isEqualTo(packagePaths.get(1).toString());
+
+    builder.setRelativePath("com/other/Test.java");
+
+    assertThat(decoder.decode(builder.buildIdeInfoArtifact()).getRootPath())
+      .isEqualTo(packagePaths.get(2).toString());
+
+    builder
+      .setRootPath(WORKSPACE_ROOT.toString())
+      .setRelativePath("third_party/other/Temp.java");
+
+    assertThat(decoder.decode(builder.buildIdeInfoArtifact())).isNull();
+  }
+
+  @Test
+  public void testDerivedArtifactAllVersions() throws Exception {
+    ArtifactLocationBuilder builder = new ArtifactLocationBuilder()
+      .setRootPath(EXECUTION_ROOT + "/blaze-out/bin")
+      .setRootExecutionPathFragment("/blaze-out/bin")
+      .setRelativePath("com/google/Bla.java")
+      .setIsSource(false)
+      .setVersion(Version.Current);
+
+    ArtifactLocationDecoder decoder = new ArtifactLocationDecoder(BLAZE_CITC_ROOTS, null);
+
+    ArtifactLocation parsed = decoder.decode(builder.buildIdeInfoArtifact());
+
+    assertThat(parsed)
+      .isEqualTo(decoder.decode(builder.buildManifestArtifact()));
+
+    assertThat(parsed).isEqualTo(
+      ArtifactLocation.builder()
+        .setRootPath(EXECUTION_ROOT + "/blaze-out/bin")
+        .setRootExecutionPathFragment("/blaze-out/bin")
+        .setRelativePath("com/google/Bla.java")
+        .setIsSource(false)
+        .build());
+
+    assertThat(parsed).isEqualTo(
+      decoder.decode(
+        builder.setVersion(Version.Past).buildIdeInfoArtifact()
+      ));
+
+    ArtifactLocation future = decoder.decode(
+      builder.setVersion(Version.Future).buildIdeInfoArtifact());
+
+    assertThat(future).isEqualTo(
+      ArtifactLocation.builder()
+        .setRootPath(EXECUTION_ROOT + "/blaze-out/bin")
+        .setRootExecutionPathFragment("/blaze-out/bin")
+        .setRelativePath("com/google/Bla.java")
+        .setIsSource(false)
+        .build());
+  }
+
+  @Test
+  public void testSourceArtifactAllVersions() throws Exception {
+    ArtifactLocationBuilder builder = new ArtifactLocationBuilder()
+      .setRootPath(WORKSPACE_ROOT.toString())
+      .setRelativePath("com/google/Bla.java")
+      .setIsSource(true)
+      .setVersion(Version.Current);
+
+    ArtifactLocationDecoder decoder = new ArtifactLocationDecoder(BLAZE_CITC_ROOTS,
+                                                                  new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_CITC_ROOTS));
+
+    ArtifactLocation parsed = decoder.decode(builder.buildIdeInfoArtifact());
+
+    assertThat(parsed)
+      .isEqualTo(decoder.decode(builder.buildManifestArtifact()));
+
+    assertThat(parsed).isEqualTo(
+      ArtifactLocation.builder()
+        .setRootPath(WORKSPACE_ROOT.toString())
+        .setRelativePath("com/google/Bla.java")
+        .setIsSource(true)
+        .build());
+
+    assertThat(parsed).isEqualTo(
+      decoder.decode(
+        builder.setVersion(Version.Past).buildIdeInfoArtifact()
+      ));
+
+    ArtifactLocation future = decoder.decode(
+      builder.setVersion(Version.Future).buildIdeInfoArtifact());
+
+    assertThat(future).isEqualTo(
+      ArtifactLocation.builder()
+        .setRootPath(WORKSPACE_ROOT.toString())
+        .setRelativePath("com/google/Bla.java")
+        .setIsSource(true)
+        .build());
+  }
+
+  enum Version {
+    Past, // no rootExecutionPathFragment
+    Current, // everything
+    Future // no rootPath
+  }
+
+  static class ArtifactLocationBuilder {
+    Version version;
+    String rootPath;
+    String rootExecutionPathFragment = "";
+    String relativePath;
+    boolean isSource;
+
+
+    ArtifactLocationBuilder setVersion(Version version) {
+      this.version = version;
+      return this;
+    }
+
+    ArtifactLocationBuilder setRootPath(String rootPath) {
+      this.rootPath = rootPath;
+      return this;
+    }
+
+    ArtifactLocationBuilder setRootExecutionPathFragment(String rootExecutionPathFragment) {
+      this.rootExecutionPathFragment = rootExecutionPathFragment;
+      return this;
+    }
+
+    ArtifactLocationBuilder setRelativePath(String relativePath) {
+      this.relativePath = relativePath;
+      return this;
+    }
+
+    ArtifactLocationBuilder setIsSource(boolean isSource) {
+      this.isSource = isSource;
+      return this;
+    }
+
+    AndroidStudioIdeInfo.ArtifactLocation buildIdeInfoArtifact() {
+      AndroidStudioIdeInfo.ArtifactLocation.Builder builder = AndroidStudioIdeInfo.ArtifactLocation.newBuilder()
+        .setIsSource(isSource)
+        .setRelativePath(relativePath);
+      if (version != Version.Past) {
+        builder.setRootExecutionPathFragment(rootExecutionPathFragment);
+      }
+      if (version != Version.Future) {
+        builder.setRootPath(rootPath);
+      }
+      return builder.build();
+    }
+
+    PackageManifestOuterClass.ArtifactLocation buildManifestArtifact() {
+      PackageManifestOuterClass.ArtifactLocation.Builder builder = PackageManifestOuterClass.ArtifactLocation.newBuilder()
+        .setIsSource(isSource)
+        .setRelativePath(relativePath);
+      if (version != Version.Past) {
+        builder.setRootExecutionPathFragment(rootExecutionPathFragment);
+      }
+      if (version != Version.Future) {
+        builder.setRootPath(rootPath);
+      }
+      return builder.build();
+    }
+  }
+
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.java
new file mode 100644
index 0000000..95b7efe
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/sync/workspace/WorkspacePathResolverImplTest.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.sync.workspace;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import org.junit.Test;
+
+import java.io.File;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class WorkspacePathResolverImplTest extends BlazeTestCase {
+  private static final WorkspaceRoot WORKSPACE_ROOT = new WorkspaceRoot(new File("/path/to/root"));
+  private static final String EXECUTION_ROOT = "/path/to/_blaze_user/1234bf129e/root";
+
+  private static final BlazeRoots BLAZE_CITC_ROOTS = new BlazeRoots(
+    new File(EXECUTION_ROOT),
+    ImmutableList.of(WORKSPACE_ROOT.directory()),
+    new ExecutionRootPath("blaze-out/crosstool/bin"),
+    new ExecutionRootPath("blaze-out/crosstool/genfiles")
+  );
+
+  @Test
+  public void testResolveToIncludeDirectories() {
+    WorkspacePathResolver workspacePathResolver = new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_CITC_ROOTS);
+    ImmutableList<File> files = workspacePathResolver.resolveToIncludeDirectories(new ExecutionRootPath("tools/fast"));
+    assertThat(files).containsExactly(new File("/path/to/root/tools/fast"));
+  }
+
+  @Test
+  public void testResolveToIncludeDirectoriesForExecRootPath() {
+    WorkspacePathResolver workspacePathResolver = new WorkspacePathResolverImpl(WORKSPACE_ROOT, BLAZE_CITC_ROOTS);
+    ImmutableList<File> files = workspacePathResolver.resolveToIncludeDirectories(
+      new ExecutionRootPath("blaze-out/crosstool/bin/tools/fast")
+    );
+    assertThat(files).containsExactly(new File("/path/to/root/blaze-out/crosstool/bin/tools/fast"));
+  }
+}
diff --git a/blaze-base/tests/unittests/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessorTest.java b/blaze-base/tests/unittests/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessorTest.java
new file mode 100644
index 0000000..8bb373e
--- /dev/null
+++ b/blaze-base/tests/unittests/com/google/idea/blaze/base/vcs/git/GitStatusLineProcessorTest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.vcs.git;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for {@link GitStatusLineProcessor}
+ */
+@RunWith(JUnit4.class)
+public class GitStatusLineProcessorTest {
+
+  @Test
+  public void testGitStatusParser() {
+    GitStatusLineProcessor lineProcessor = new GitStatusLineProcessor(new WorkspaceRoot(new File("/usr/blah")), "/usr/blah");
+    for (String line : ImmutableList.of(
+      "D    root/README",
+      "M    root/blaze-base/src/com/google/idea/blaze/base/root/citc/CitcUtil.java",
+      "A    root/blah",
+      "A    java/com/google/Test.java",
+      "M    java/com/other/"
+    )) {
+      lineProcessor.processLine(line);
+    }
+    assertThat(lineProcessor.addedFiles).containsExactly(
+      new WorkspacePath("root/blah"),
+      new WorkspacePath("java/com/google/Test.java")
+    );
+    assertThat(lineProcessor.modifiedFiles).containsExactly(
+      new WorkspacePath("root/blaze-base/src/com/google/idea/blaze/base/root/citc/CitcUtil.java"),
+      new WorkspacePath("java/com/other")
+    );
+    assertThat(lineProcessor.deletedFiles).containsExactly(
+      new WorkspacePath("root/README")
+    );
+  }
+
+  @Test
+  public void testGitStatusParserDifferentRoots() {
+    GitStatusLineProcessor lineProcessor = new GitStatusLineProcessor(new WorkspaceRoot(new File("/usr/blah/root")), "/usr/blah");
+    for (String line : ImmutableList.of(
+      "D    root/README",
+      "M    root/blaze-base/src/com/google/idea/blaze/base/root/citc/CitcUtil.java",
+      "A    root/blah",
+      "A    java/com/google/Test.java",
+      "M    java/com/other/"
+    )) {
+      lineProcessor.processLine(line);
+    }
+    assertThat(lineProcessor.addedFiles).containsExactly(
+      new WorkspacePath("blah")
+    );
+    assertThat(lineProcessor.modifiedFiles).containsExactly(
+      new WorkspacePath("blaze-base/src/com/google/idea/blaze/base/root/citc/CitcUtil.java")
+    );
+    assertThat(lineProcessor.deletedFiles).containsExactly(
+      new WorkspacePath("README")
+    );
+  }
+}
diff --git a/blaze-base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java b/blaze-base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
new file mode 100644
index 0000000..16cda1f
--- /dev/null
+++ b/blaze-base/tests/utils/integration/com/google/idea/blaze/base/BlazeIntegrationTestCase.java
@@ -0,0 +1,479 @@
+/*
+ * 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;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.google.idea.blaze.base.lang.buildfile.search.FindUsages;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.base.sync.projectstructure.ModuleEditorProvider;
+
+import com.intellij.codeInsight.lookup.Lookup;
+import com.intellij.codeInsight.lookup.LookupElement;
+import com.intellij.codeInsight.lookup.LookupElementPresentation;
+import com.intellij.openapi.actionSystem.IdeActions;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.application.PathManager;
+import com.intellij.openapi.command.CommandProcessor;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.extensions.ExtensionPoint;
+import com.intellij.openapi.extensions.ExtensionPointName;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.ProjectJdkTable;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem;
+import com.intellij.psi.*;
+import com.intellij.psi.impl.source.PostprocessReformattingAspect;
+import com.intellij.refactoring.move.moveClassesOrPackages.MoveDirectoryWithClassesProcessor;
+import com.intellij.testFramework.*;
+import com.intellij.testFramework.EditorTestUtil.CaretAndSelectionState;
+import com.intellij.testFramework.EditorTestUtil.CaretInfo;
+import com.intellij.testFramework.fixtures.*;
+import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl;
+import org.picocontainer.MutablePicoContainer;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Base test class for blaze integration tests.
+ */
+public abstract class BlazeIntegrationTestCase extends UsefulTestCase {
+
+  private static final LightProjectDescriptor projectDescriptor = LightCodeInsightFixtureTestCase.JAVA_8;
+
+  private static boolean isRunThroughBlaze() {
+    return System.getenv("JAVA_RUNFILES") != null;
+  }
+
+  protected CodeInsightTestFixture testFixture;
+  protected WorkspaceRoot workspaceRoot;
+  private String oldPluginPathProperty;
+
+  @Override
+  protected final void setUp() throws Exception {
+    if (!isRunThroughBlaze()) {
+      // If running directly through the IDE, don't try to load plugins from the sandbox environment.
+      // Instead we'll rely on the slightly more hermetic module classpath
+      oldPluginPathProperty = System.getProperty(PathManager.PROPERTY_PLUGINS_PATH);
+      System.setProperty(PathManager.PROPERTY_PLUGINS_PATH, "/dev/null");
+    }
+
+    super.setUp();
+
+    IdeaTestFixtureFactory factory = IdeaTestFixtureFactory.getFixtureFactory();
+    TestFixtureBuilder<IdeaProjectTestFixture> fixtureBuilder = factory.createLightFixtureBuilder(projectDescriptor);
+    final IdeaProjectTestFixture fixture = fixtureBuilder.getFixture();
+    testFixture = factory.createCodeInsightFixture(fixture, createTempDirFixture());
+    testFixture.setUp();
+
+    ApplicationManager.getApplication().runWriteAction(() -> ProjectJdkTable.getInstance().addJdk(IdeaTestUtil.getMockJdk18()));
+
+    workspaceRoot = new WorkspaceRoot(new File(LightPlatformTestCase.getSourceRoot().getPath()));
+    setBlazeImportSettings(new BlazeImportSettings(
+      workspaceRoot.toString(),
+      "test-project",
+      workspaceRoot + "/project-data-dir",
+      "location-hash",
+      workspaceRoot + "/project-view-file",
+      buildSystem()
+    ));
+
+    registerApplicationService(FileAttributeProvider.class, new TempFileAttributeProvider());
+    registerApplicationService(InputStreamProvider.class, file -> {
+      VirtualFile vf = findFile(file.getPath());
+      if (vf == null) {
+        throw new FileNotFoundException();
+      }
+      return vf.getInputStream();
+    });
+
+    doSetup();
+  }
+
+  /**
+   * Override to run tests with bazel specified as the project's build system.
+   */
+  protected BuildSystem buildSystem() {
+    return BuildSystem.Blaze;
+  }
+
+  protected void doSetup() throws Exception {
+  }
+
+  @Override
+  protected final void tearDown() throws Exception {
+    if (oldPluginPathProperty != null) {
+      System.setProperty(PathManager.PROPERTY_PLUGINS_PATH, oldPluginPathProperty);
+    } else {
+      System.clearProperty(PathManager.PROPERTY_PLUGINS_PATH);
+    }
+    testFixture.tearDown();
+    testFixture = null;
+    super.tearDown();
+    clearFields(this);
+    doTearDown();
+  }
+
+  protected void doTearDown() throws Exception {
+  }
+
+  protected void setBlazeImportSettings(BlazeImportSettings importSettings) {
+    BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(importSettings);
+  }
+
+  /**
+   * @return fixture to be used as temporary dir.
+   */
+  protected TempDirTestFixture createTempDirFixture() {
+    return new LightTempDirTestFixtureImpl(true); // "tmp://src/" dir by default
+  }
+
+  /**
+   * Absolute file paths are prohibited -- the TempDirTestFixture used in these tests
+   * will prepend it's own root to the path.
+   */
+  protected void assertPathIsNotAbsolute(String filePath) {
+    assertThat(FileUtil.isAbsolute(filePath)).isFalse();
+  }
+
+  /**
+   * Creates a file with the specified contents and file path in the test project
+   */
+  protected VirtualFile createFile(String filePath) {
+    return testFixture.getTempDirFixture().createFile(filePath);
+  }
+
+  /**
+   * Creates a file with the specified contents and file path in the test project
+   */
+  protected VirtualFile createFile(String filePath, String... contentLines) {
+    return createFile(filePath, Joiner.on("\n").join(contentLines));
+  }
+
+  /**
+   * Creates a file with the specified contents and file path in the test project
+   */
+  protected VirtualFile createFile(String filePath, String contents) {
+    assertPathIsNotAbsolute(filePath);
+    try {
+      return testFixture.getTempDirFixture().createFile(filePath, contents);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  protected PsiDirectory createPsiDirectory(String path) {
+    return getPsiDirectory(createDirectory(path));
+  }
+
+  protected VirtualFile createDirectory(String path) {
+    assertPathIsNotAbsolute(path);
+    try {
+      return testFixture.getTempDirFixture().findOrCreateDir(path);
+    }
+    catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  protected Editor openFileInEditor(PsiFile file) {
+    return openFileInEditor(file.getVirtualFile());
+  }
+
+  protected Editor openFileInEditor(VirtualFile file) {
+    testFixture.openFileInEditor(file);
+    return testFixture.getEditor();
+  }
+
+  /**
+   * @return null if the only item was auto-completed
+   */
+  @Nullable
+  protected String[] getCompletionItemsAsStrings() {
+    LookupElement[] completionItems = testFixture.completeBasic();
+    if (completionItems == null) {
+      return null;
+    }
+    return Arrays.stream(completionItems)
+      .map(LookupElement::getLookupString)
+      .toArray(String[]::new);
+  }
+
+  /**
+   * @return null if the only item was auto-completed
+   */
+  @Nullable
+  protected String[] getCompletionItemsAsSuggestionStrings() {
+    LookupElement[] completionItems = testFixture.completeBasic();
+    if (completionItems == null) {
+      return null;
+    }
+    LookupElementPresentation presentation = new LookupElementPresentation();
+    String[] strings = new String[completionItems.length];
+    for (int i = 0; i < strings.length; i++) {
+      completionItems[i].renderElement(presentation);
+      strings[i] = presentation.getItemText();
+    }
+    return strings;
+  }
+
+  /**
+   * @return true if a LookupItem was inserted.
+   */
+  protected boolean completeIfUnique() {
+    LookupElement[] completionItems = testFixture.completeBasic();
+    if (completionItems == null) {
+      return true;
+    }
+    if (completionItems.length != 1) {
+      return false;
+    }
+    testFixture.getLookup().setCurrentItem(completionItems[0]);
+    testFixture.finishLookup(Lookup.NORMAL_SELECT_CHAR);
+    return true;
+  }
+
+  /**
+   * Simulates a user typing action, at current caret position of file.
+   */
+  protected void performTypingAction(PsiFile file, char typedChar) {
+    performTypingAction(openFileInEditor(file.getVirtualFile()), typedChar);
+  }
+
+  /**
+   * Simulates a user typing action, at current caret position of document.
+   */
+  protected void performTypingAction(Editor editor, char typedChar) {
+    EditorTestUtil.performTypingAction(editor, typedChar);
+    getProject().getComponent(PostprocessReformattingAspect.class).doPostponedFormatting();
+    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
+  }
+
+  /**
+   * Clicks the specified button in current document at the current caret position
+   *
+   * @param action which button to click (see {@link IdeActions})
+   */
+  protected final void pressButton(final String action) {
+    CommandProcessor.getInstance().executeCommand(
+      getProject(),
+      () -> testFixture.performEditorAction(action),
+      "",
+      null);
+  }
+
+  protected void setCaretPosition(Editor editor, int lineNumber, int columnNumber) {
+    CaretInfo info = new CaretInfo(new LogicalPosition(lineNumber, columnNumber), null);
+    EditorTestUtil.setCaretsAndSelection(editor, new CaretAndSelectionState(ImmutableList.of(info), null));
+  }
+
+  protected void assertCaretPosition(Editor editor, int lineNumber, int columnNumber) {
+    CaretInfo info = new CaretInfo(new LogicalPosition(lineNumber, columnNumber), null);
+    EditorTestUtil.verifyCaretAndSelectionState(editor, new CaretAndSelectionState(ImmutableList.of(info), null));
+  }
+
+  protected Project getProject() {
+    return testFixture.getProject();
+  }
+
+  protected VirtualFile findFile(String filePath) {
+    VirtualFile vf = TempFileSystem.getInstance().findFileByPath(filePath);
+    if (vf == null) {
+      // this might be a relative path
+      vf = testFixture.getTempDirFixture().getFile(filePath);
+    }
+    return vf;
+  }
+
+  protected void assertFileContents(String filePath, String... contentLines) {
+    assertFileContents(findFile(filePath), contentLines);
+  }
+
+  protected void assertFileContents(VirtualFile file, String... contentLines) {
+    assertFileContents(getPsiFile(file), contentLines);
+  }
+
+  protected void assertFileContents(PsiFile file, String... contentLines) {
+    String contents = Joiner.on("\n").join(contentLines);
+    assertThat(file.getText()).isEqualTo(contents);
+  }
+
+  /**
+   * Creates a file with the specified contents and file path in the test project
+   */
+  protected PsiFile createPsiFile(String filePath) {
+    return getPsiFile(testFixture.getTempDirFixture().createFile(filePath));
+  }
+
+  /**
+   * Creates a file with the specified contents and file path in the test project
+   */
+  protected PsiFile createPsiFile(String filePath, String... contentLines) {
+    return getPsiFile(createFile(filePath, contentLines));
+  }
+
+  /**
+   * Finds PsiFile, and asserts that it's not null.
+   */
+  protected PsiFile getPsiFile(VirtualFile file) {
+    PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(file);
+    assertThat(psiFile).isNotNull();
+    return psiFile;
+  }
+
+  /**
+   * Finds PsiDirectory, and asserts that it's not null.
+   */
+  protected PsiDirectory getPsiDirectory(VirtualFile file) {
+    PsiDirectory psiFile = PsiManager.getInstance(getProject()).findDirectory(file);
+    assertThat(psiFile).isNotNull();
+    return psiFile;
+  }
+
+  protected PsiDirectory renameDirectory(String oldPath, String newPath) {
+    try {
+      VirtualFile original = findFile(oldPath);
+      PsiDirectory originalPsi = PsiManager.getInstance(getProject()).findDirectory(original);
+      assertThat(originalPsi).isNotNull();
+
+      VirtualFile destination = testFixture.getTempDirFixture().findOrCreateDir(newPath);
+      PsiDirectory destPsi = PsiManager.getInstance(getProject()).findDirectory(destination);
+      assertThat(destPsi).isNotNull();
+
+      new MoveDirectoryWithClassesProcessor(getProject(), new PsiDirectory[] {originalPsi}, destPsi, true, true, false, null).run();
+      return destPsi;
+
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  protected void renamePsiElement(PsiNamedElement element, String newName) {
+    testFixture.renameElement(element, newName);
+  }
+
+  protected void handleRename(PsiReference reference, String newName) {
+    doRenameOperation(() -> reference.handleElementRename(newName));
+  }
+
+  protected void doRenameOperation(Runnable renameOp) {
+    ApplicationManager.getApplication().runWriteAction(
+      () -> CommandProcessor.getInstance().runUndoTransparentAction(renameOp));
+  }
+
+  protected static <T> List<T> findAllReferencingElementsOfType(PsiElement target, Class<T> referenceType) {
+    return Arrays.stream(FindUsages.findAllReferences(target))
+      .map(PsiReference::getElement)
+      .filter(referenceType::isInstance)
+      .map(e -> (T) e)
+      .collect(Collectors.toList());
+  }
+
+  protected void mockBlazeProjectDataManager(BlazeProjectData data) {
+    BlazeProjectDataManager mockProjectDataManager = new BlazeProjectDataManager() {
+      @Nullable
+      @Override
+      public BlazeProjectData getBlazeProjectData() {
+        return data;
+      }
+      @Override
+      public BlazeSyncPlugin.ModuleEditor editModules() {
+        return ModuleEditorProvider.getInstance().getModuleEditor(
+          getProject(),
+          BlazeImportSettingsManager.getInstance(getProject()).getImportSettings()
+        );
+      }
+    };
+    registerProjectService(BlazeProjectDataManager.class, mockProjectDataManager);
+  }
+
+  protected <T> void registerApplicationService(Class<T> key, T implementation) {
+    registerComponentInstance((MutablePicoContainer) ApplicationManager.getApplication().getPicoContainer(), key, implementation);
+  }
+
+  protected <T> void registerProjectService(Class<T> key, T implementation) {
+    registerComponentInstance((MutablePicoContainer) getProject().getPicoContainer(), key, implementation);
+  }
+
+  protected <T> void registerComponentInstance(MutablePicoContainer container, Class<T> key, T implementation) {
+    Object old = container.getComponentInstance(key);
+    container.unregisterComponent(key.getName());
+    container.registerComponentInstance(key.getName(), implementation);
+    Disposer.register(getTestRootDisposable(), () -> {
+      container.unregisterComponent(key.getName());
+      if (old != null) {
+        container.registerComponentInstance(key.getName(), old);
+      }
+    });
+  }
+
+  protected <T> void registerExtension(ExtensionPointName<T> name, T instance) {
+    ExtensionPoint<T> ep = Extensions.getRootArea().getExtensionPoint(name);
+    ep.registerExtension(instance);
+    Disposer.register(getTestRootDisposable(), () -> ep.unregisterExtension(instance));
+  }
+
+  /**
+   * Redirects file system checks via the TempFileSystem used for these tests.
+   */
+  private static class TempFileAttributeProvider extends FileAttributeProvider {
+
+    final TempFileSystem fileSystem = TempFileSystem.getInstance();
+
+    @Override
+    public boolean exists(File file) {
+      VirtualFile vf = getVirtualFile(file);
+      return vf != null && vf.exists();
+    }
+
+    @Override
+    public boolean isDirectory(File file) {
+      VirtualFile vf = getVirtualFile(file);
+      return vf != null && vf.isDirectory();
+    }
+
+    @Override
+    public boolean isFile(File file) {
+      VirtualFile vf = getVirtualFile(file);
+      return vf != null && vf.exists() && !vf.isDirectory();
+    }
+
+    private VirtualFile getVirtualFile(File file) {
+      return fileSystem.findFileByPath(file.getPath());
+    }
+  }
+
+}
diff --git a/blaze-base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java b/blaze-base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java
new file mode 100644
index 0000000..6db82a3
--- /dev/null
+++ b/blaze-base/tests/utils/integration/com/google/idea/blaze/base/lang/buildfile/BuildFileIntegrationTestCase.java
@@ -0,0 +1,78 @@
+/*
+ * 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;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.StringLiteral;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import com.intellij.lang.ASTNode;
+import com.intellij.psi.PsiFile;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * BUILD file specific integration test base
+ */
+public abstract class BuildFileIntegrationTestCase extends BlazeIntegrationTestCase {
+
+  @Override
+  protected void doSetup() {
+    mockBlazeProjectDataManager(getMockBlazeProjectData());
+  }
+
+  /**
+   * Creates a file with the specified contents and file path in the test project,
+   * and asserts that it's parsed as a BuildFile
+   */
+  protected BuildFile createBuildFile(String filePath, String... contentLines) {
+    PsiFile file = createPsiFile(filePath, contentLines);
+    assertThat(file).isInstanceOf(BuildFile.class);
+    return (BuildFile) file;
+  }
+
+  protected void replaceStringContents(StringLiteral string, String newStringContents) {
+    doRenameOperation(() -> {
+      ASTNode node = string.getNode();
+      node.replaceChild(node.getFirstChildNode(), PsiUtils.createNewLabel(string.getProject(), newStringContents));
+    });
+  }
+
+  private BlazeProjectData getMockBlazeProjectData() {
+    BlazeRoots fakeRoots = new BlazeRoots(
+      null,
+      ImmutableList.of(workspaceRoot.directory()),
+      new ExecutionRootPath("out/crosstool/bin"),
+      new ExecutionRootPath("out/crosstool/gen")
+    );
+    return new BlazeProjectData(0,
+                                ImmutableMap.of(),
+                                fakeRoots,
+                                new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()),
+                                new WorkspacePathResolverImpl(workspaceRoot, fakeRoots),
+                                null,
+                                null,
+                                null);
+  }
+
+}
diff --git a/blaze-base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java b/blaze-base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
new file mode 100644
index 0000000..1ca2255
--- /dev/null
+++ b/blaze-base/tests/utils/integration/com/google/idea/blaze/base/sync/BlazeSyncIntegrationTestCase.java
@@ -0,0 +1,316 @@
+/*
+ * 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.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.idea.blaze.base.BlazeIntegrationTestCase;
+import com.google.idea.blaze.base.command.info.BlazeInfo;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.io.WorkspaceScanner;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.aspects.BlazeIdeInterface;
+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;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import com.google.idea.blaze.base.vcs.BlazeVcsHandler;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.testFramework.fixtures.TempDirTestFixture;
+import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Sets up mocks required for integration tests of the blaze sync process.
+ */
+public abstract class BlazeSyncIntegrationTestCase extends BlazeIntegrationTestCase {
+
+  // root directory for all files outside the project directory.
+  protected TempDirTestFixture tempDirectoryHandler;
+  protected VirtualFile tempDirectory;
+
+  // blaze-info data
+  private static final String EXECUTION_ROOT = "/execroot/root";
+  private static final String BLAZE_BIN = EXECUTION_ROOT + "/blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
+  private static final String BLAZE_GENFILES = EXECUTION_ROOT + "/blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/genfiles";
+
+  private static final String PROJECT_DATA_DIR = "project-data-dir";
+
+  private MockProjectViewManager projectViewManager;
+  private MockBlazeVcsHandler vcsHandler;
+  private MockBlazeInfo blazeInfoData;
+  private MockBlazeIdeInterface blazeIdeInterface;
+
+  protected ErrorCollector errorCollector;
+  protected BlazeContext context;
+
+  @Override
+  protected void doSetup() throws IOException {
+    // Set up a workspace root outside of the tracked temp file system.
+    tempDirectoryHandler = new LightTempDirTestFixtureImpl();
+    tempDirectory = tempDirectoryHandler.getFile("");
+    workspaceRoot = new WorkspaceRoot(new File(tempDirectory.getPath()));
+    setBlazeImportSettings(new BlazeImportSettings(
+      workspaceRoot.toString(),
+      "test-project",
+      workspaceRoot + "/"+ PROJECT_DATA_DIR,
+      "location-hash",
+      workspaceRoot + "/project-view-file",
+      BuildSystem.Blaze
+    ));
+
+    projectViewManager = new MockProjectViewManager();
+    vcsHandler = new MockBlazeVcsHandler();
+    blazeInfoData = new MockBlazeInfo();
+    blazeIdeInterface = new MockBlazeIdeInterface();
+    registerProjectService(ProjectViewManager.class, projectViewManager);
+    registerExtension(BlazeVcsHandler.EP_NAME, vcsHandler);
+    registerApplicationService(WorkspaceScanner.class, (workspaceRoot, workspacePath) -> true);
+    registerApplicationService(BlazeInfo.class, blazeInfoData);
+    registerApplicationService(BlazeIdeInterface.class, blazeIdeInterface);
+    registerApplicationService(ModuleEditorProvider.class, new ModuleEditorProvider() {
+      @Override
+      public ModuleEditorImpl getModuleEditor(Project project, BlazeImportSettings importSettings) {
+        return new ModuleEditorImpl(project, importSettings) {
+          @Override
+          public void commit() {
+            // don't commit module changes, but make sure they're properly disposed when the test is finished
+            for (ModifiableRootModel model : modifiableModels) {
+              Disposer.register(myTestRootDisposable, model::dispose);
+            }
+          }
+        };
+      }
+    });
+
+    errorCollector = new ErrorCollector();
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+
+    tempDirectoryHandler.findOrCreateDir(PROJECT_DATA_DIR + "/.blaze/modules");
+
+    setBlazeInfoResults(ImmutableMap.of(
+      BlazeInfo.blazeBinKey(Blaze.getBuildSystem(getProject())), BLAZE_BIN,
+      BlazeInfo.blazeGenfilesKey(Blaze.getBuildSystem(getProject())), BLAZE_GENFILES,
+      BlazeInfo.EXECUTION_ROOT_KEY, EXECUTION_ROOT,
+      BlazeInfo.PACKAGE_PATH_KEY, workspaceRoot.toString()
+    ));
+  }
+
+  @Override
+  protected void doTearDown() throws Exception {
+    if (tempDirectoryHandler != null) {
+      tempDirectoryHandler.tearDown();
+    }
+    super.doTearDown();
+  }
+
+  protected VirtualFile createWorkspaceFile(String relativePath, @Nullable String... contents) {
+    try {
+      String content = contents != null ? Joiner.on("\n").join(contents) : "";
+      return tempDirectoryHandler.createFile(relativePath, content);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  protected void assertNoErrors() {
+    errorCollector.assertNoIssues();
+  }
+
+  protected ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder()
+      .setRootPath(workspaceRoot.toString())
+      .setRelativePath(relativePath)
+      .setIsSource(true)
+      .build();
+  }
+
+  protected void setProjectView(String... contents) {
+    ProjectViewParser projectViewParser = new ProjectViewParser(context, new WorkspacePathResolverImpl(workspaceRoot));
+    projectViewParser.parseProjectView(Joiner.on("\n").join(contents));
+
+    ProjectViewSet result = projectViewParser.getResult();
+    assertThat(result.getProjectViewFiles()).isNotEmpty();
+    assertNoErrors();
+    setProjectViewSet(result);
+  }
+
+  protected void setProjectViewSet(ProjectViewSet projectViewSet) {
+    projectViewManager.projectViewSet = projectViewSet;
+  }
+
+  protected void setRuleMap(Map<Label, RuleIdeInfo> rules) {
+    blazeIdeInterface.ruleMap.clear();
+    blazeIdeInterface.ruleMap.putAll(rules);
+  }
+
+  protected void setBlazeInfoResults(Map<String, String> blazeInfoResults) {
+    blazeInfoData.setResults(blazeInfoResults);
+  }
+
+  protected void runBlazeSync(BlazeSyncParams syncParams) {
+    Project project = getProject();
+    final BlazeSyncTask syncTask = new BlazeSyncTask(
+      project,
+      BlazeImportSettingsManager.getInstance(project).getImportSettings(),
+      syncParams);
+    syncTask.syncProject(context);
+  }
+
+  private static class MockProjectViewManager extends ProjectViewManager {
+
+    private ProjectViewSet projectViewSet;
+
+    @Nullable
+    @Override
+    public ProjectViewSet getProjectViewSet() {
+      return projectViewSet;
+    }
+
+    @Nullable
+    @Override
+    public ProjectViewSet reloadProjectView(BlazeContext context, WorkspacePathResolver workspacePathResolver) {
+      return getProjectViewSet();
+    }
+  }
+
+  private class MockBlazeVcsHandler implements BlazeVcsHandler {
+
+    private List<WorkspacePath> addedFiles = Lists.newArrayList();
+
+    @Nullable
+    @Override
+    public String getClientName(WorkspaceRoot workspaceRoot) {
+      return null;
+    }
+
+    @Override
+    public boolean handlesProject(Project project, WorkspaceRoot workspaceRoot) {
+      return project == getProject();
+    }
+
+    @Override
+    public ListenableFuture<WorkingSet> getWorkingSet(Project project, WorkspaceRoot workspaceRoot, ListeningExecutorService executor) {
+      WorkingSet workingSet = new WorkingSet(ImmutableList.copyOf(addedFiles), ImmutableList.of(), ImmutableList.of());
+      return Futures.immediateFuture(workingSet);
+    }
+
+    @Nullable
+    @Override
+    public BlazeVcsSyncHandler createSyncHandler(Project project,
+                                                 WorkspaceRoot workspaceRoot) {
+      return null;
+    }
+  }
+
+  protected static class MockBlazeInfo extends BlazeInfo {
+    private final Map<String, String> results = Maps.newHashMap();
+
+    @Override
+    public ListenableFuture<String> runBlazeInfo(@Nullable BlazeContext context,
+                                                 BuildSystem buildSystem,
+                                                 WorkspaceRoot workspaceRoot,
+                                                 List<String> blazeFlags,
+                                                 String key) {
+      return Futures.immediateFuture(results.get(key));
+    }
+
+    @Override
+    public ListenableFuture<byte[]> runBlazeInfoGetBytes(@Nullable BlazeContext context,
+                                                         BuildSystem buildSystem,
+                                                         WorkspaceRoot workspaceRoot,
+                                                         List<String> blazeFlags,
+                                                         String key) {
+      return Futures.immediateFuture(null);
+    }
+
+    @Override
+    public ListenableFuture<ImmutableMap<String, String>> runBlazeInfo(@Nullable BlazeContext context,
+                                                                       BuildSystem buildSystem,
+                                                                       WorkspaceRoot workspaceRoot,
+                                                                       List<String> blazeFlags) {
+      return Futures.immediateFuture(ImmutableMap.copyOf(results));
+    }
+
+    public void setResults(Map<String, String> results) {
+      this.results.clear();
+      this.results.putAll(results);
+    }
+  }
+
+  protected static class MockBlazeIdeInterface extends BlazeIdeInterface {
+    private final Map<Label, RuleIdeInfo> ruleMap = Maps.newHashMap();
+
+    @Nullable
+    @Override
+    public IdeResult updateBlazeIdeState(Project project,
+                                         BlazeContext context,
+                                         WorkspaceRoot workspaceRoot,
+                                         ProjectViewSet projectViewSet,
+                                         List<TargetExpression> targets,
+                                         WorkspaceLanguageSettings workspaceLanguageSettings,
+                                         ArtifactLocationDecoder artifactLocationDecoder,
+                                         SyncState.Builder syncStateBuilder,
+                                         @Nullable SyncState previousSyncState,
+                                         boolean requiresAndroidSdk) {
+      return new IdeResult(ImmutableMap.copyOf(ruleMap), null);
+    }
+
+    @Override
+    public void resolveIdeArtifacts(Project project,
+                                    BlazeContext context,
+                                    WorkspaceRoot workspaceRoot,
+                                    ProjectViewSet projectViewSet,
+                                    List<TargetExpression> targets) {
+    }
+  }
+
+}
diff --git a/blaze-base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java
new file mode 100644
index 0000000..b1a90aa
--- /dev/null
+++ b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/BlazeTestCase.java
@@ -0,0 +1,107 @@
+/*
+ * 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;
+
+import com.intellij.mock.MockProject;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.extensions.*;
+import com.intellij.openapi.extensions.impl.ExtensionPointImpl;
+import com.intellij.openapi.extensions.impl.ExtensionsAreaImpl;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Disposer;
+import org.jetbrains.annotations.NotNull;
+import org.junit.After;
+import org.junit.Before;
+import org.picocontainer.MutablePicoContainer;
+
+/**
+ * Test base class.
+ * <p/>
+ * <p>Provides a mock application and a mock project.
+ */
+public class BlazeTestCase {
+
+  protected Project project;
+  private ExtensionsAreaImpl extensionsArea;
+  private Disposable testDisposable;
+
+  private static class RootDisposable implements Disposable {
+    @Override
+    public void dispose() {
+    }
+  }
+
+  public static class Container {
+    private final MutablePicoContainer container;
+
+    Container(@NotNull MutablePicoContainer container) {
+      this.container = container;
+    }
+
+    public <T> Container register(Class<T> klass, T instance) {
+      this.container.registerComponentInstance(klass.getName(), instance);
+      return this;
+    }
+  }
+
+  @Before
+  public final void setup() {
+    testDisposable = new RootDisposable();
+    TestUtils.createMockApplication(testDisposable);
+    MutablePicoContainer applicationContainer = (MutablePicoContainer)
+      ApplicationManager.getApplication().getPicoContainer();
+    MockProject mockProject = TestUtils.mockProject(applicationContainer, testDisposable);
+
+    Extensions.cleanRootArea(testDisposable);
+    extensionsArea = (ExtensionsAreaImpl) Extensions.getRootArea();
+
+    this.project = mockProject;
+
+    initTest(
+      new Container(applicationContainer),
+      new Container(mockProject.getPicoContainer())
+    );
+  }
+
+  @After
+  public final void tearDown() {
+    Disposer.dispose(testDisposable);
+  }
+
+  public final Project getProject() {
+    return project;
+  }
+
+  protected void initTest(
+    @NotNull Container applicationServices,
+    @NotNull Container projectServices) {
+  }
+
+  protected <T> ExtensionPointImpl<T> registerExtensionPoint(@NotNull ExtensionPointName<T> name, @NotNull Class<T> type) {
+    ExtensionPointImpl<T> extensionPoint = new ExtensionPointImpl<T>(
+      name.getName(),
+      type.getName(),
+      ExtensionPoint.Kind.INTERFACE,
+      extensionsArea,
+      null,
+      new Extensions.SimpleLogProvider(),
+      new DefaultPluginDescriptor(PluginId.getId(type.getName()), type.getClassLoader())
+    );
+    extensionsArea.registerExtensionPoint(extensionPoint);
+    return extensionPoint;
+  }
+}
diff --git a/blaze-base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.java
new file mode 100644
index 0000000..c04062c
--- /dev/null
+++ b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/TestUtils.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;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.intellij.mock.MockApplicationEx;
+import com.intellij.mock.MockProject;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.application.Application;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.extensions.Extensions;
+import com.intellij.openapi.fileTypes.FileTypeManager;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.vfs.encoding.EncodingManager;
+import com.intellij.openapi.vfs.encoding.EncodingManagerImpl;
+import com.intellij.util.pico.DefaultPicoContainer;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.picocontainer.PicoContainer;
+
+import java.io.*;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Future;
+
+import static org.junit.Assert.fail;
+
+/**
+ * Test utilities.
+ */
+public class TestUtils {
+
+  static class BlazeMockApplication extends MockApplicationEx {
+    private final ListeningExecutorService executor = MoreExecutors.sameThreadExecutor();
+
+    public BlazeMockApplication(@NotNull Disposable parentDisposable) {
+      super(parentDisposable);
+    }
+
+    @NotNull
+    @Override
+    public Future<?> executeOnPooledThread(@NotNull Runnable action) {
+      return executor.submit(action);
+    }
+
+    @NotNull
+    @Override
+    public <T> Future<T> executeOnPooledThread(@NotNull Callable<T> action) {
+      return executor.submit(action);
+    }
+  }
+
+  public static void createMockApplication(Disposable parentDisposable) {
+    final BlazeMockApplication instance = new BlazeMockApplication(parentDisposable);
+
+    // If there was no previous application, ApplicationManager leaves the MockApplication in place, which can break future tests.
+    Application oldApplication = ApplicationManager.getApplication();
+    if (oldApplication == null) {
+      Disposer.register(parentDisposable, () -> {
+        new ApplicationManager() {
+          { ourApplication = null; }
+        };
+      });
+    }
+
+    ApplicationManager.setApplication(instance,
+                                      FileTypeManager::getInstance,
+                                      parentDisposable);
+    instance.registerService(EncodingManager.class, EncodingManagerImpl.class);
+  }
+
+  @NotNull
+  public static MockProject mockProject(@Nullable PicoContainer container,
+                                        Disposable parentDisposable) {
+    Extensions.registerAreaClass("IDEA_PROJECT", null);
+    container = container != null
+                ? container
+                : new DefaultPicoContainer();
+    return new MockProject(container, parentDisposable);
+  }
+
+  public static void assertIsSerializable(@NotNull Serializable object) {
+    ObjectOutputStream out = null;
+    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+    try {
+      out = new ObjectOutputStream(byteArrayOutputStream);
+      out.writeObject(object);
+    }
+    catch (NotSerializableException e) {
+      fail("An object is not serializable: " + e.getMessage());
+    }
+    catch (IOException e) {
+      fail("Could not serialize object: " + e.getMessage());
+    }
+    finally {
+      if (out != null) {
+        try {
+          out.close();
+        }
+        catch (IOException e) {
+          // ignore
+        }
+      }
+      try {
+        byteArrayOutputStream.close();
+      }
+      catch (IOException e) {
+        // ignore
+      }
+    }
+  }
+
+}
diff --git a/blaze-base/tests/utils/unit/com/google/idea/blaze/base/async/executor/MockBlazeExecutor.java b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/async/executor/MockBlazeExecutor.java
new file mode 100644
index 0000000..b7c8691
--- /dev/null
+++ b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/async/executor/MockBlazeExecutor.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.async.executor;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.Callable;
+
+/**
+ * Used in tests.
+ */
+public class MockBlazeExecutor extends BlazeExecutor {
+
+  private final ListeningExecutorService executor = MoreExecutors.sameThreadExecutor();
+
+  @Override
+  public <T> ListenableFuture<T> submit(final Callable<T> callable) {
+    return executor.submit(callable);
+  }
+
+  @Override
+  public ListeningExecutorService getExecutor() {
+    return executor;
+  }
+}
diff --git a/blaze-base/tests/utils/unit/com/google/idea/blaze/base/experiments/MockExperimentService.java b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/experiments/MockExperimentService.java
new file mode 100644
index 0000000..ece1b23
--- /dev/null
+++ b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/experiments/MockExperimentService.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.experiments;
+
+import com.google.common.collect.Maps;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Map;
+
+/**
+ * Used for tests.
+ */
+public class MockExperimentService implements ExperimentService {
+
+  private Map<String, Object> experiments = Maps.newHashMap();
+
+  @Override
+  public void reloadExperiments() {
+  }
+
+  @Override
+  public void startExperimentScope() {
+  }
+
+  @Override
+  public void endExperimentScope() {
+  }
+
+  public void setExperiment(@NotNull BoolExperiment experiment, @NotNull boolean value) {
+    experiments.put(experiment.getKey(), value);
+  }
+
+  @Override
+  public boolean getExperiment(@NotNull String key, boolean defaultValue) {
+    if (experiments.containsKey(key)) {
+      return (Boolean)experiments.get(key);
+    }
+    return defaultValue;
+  }
+
+  @Override
+  @Nullable
+  public String getExperimentString(@NotNull String key, @Nullable String defaultValue) {
+    if (experiments.containsKey(key)) {
+      return (String)experiments.get(key);
+    }
+    return defaultValue;
+  }
+
+  @Override
+  public int getExperimentInt(@NotNull String key, int defaultValue) {
+    if (experiments.containsKey(key)) {
+      return (Integer)experiments.get(key);
+    }
+    return defaultValue;
+  }
+}
diff --git a/blaze-base/tests/utils/unit/com/google/idea/blaze/base/ideinfo/RuleMapBuilder.java b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/ideinfo/RuleMapBuilder.java
new file mode 100644
index 0000000..fd20f3f
--- /dev/null
+++ b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/ideinfo/RuleMapBuilder.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.ideinfo;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.Label;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Builds a rule map.
+ */
+public class RuleMapBuilder {
+  private List<RuleIdeInfo> rules = Lists.newArrayList();
+
+  public static RuleMapBuilder builder() {
+    return new RuleMapBuilder();
+  }
+
+  @NotNull
+  public RuleMapBuilder addRule(@NotNull RuleIdeInfo ruleOrLibrary) {
+    rules.add(ruleOrLibrary);
+    return this;
+  }
+  @NotNull
+  public RuleMapBuilder addRule(@NotNull RuleIdeInfo.Builder ruleOrLibrary) {
+    return addRule(ruleOrLibrary.build());
+  }
+
+  @NotNull
+  public ImmutableMap<Label, RuleIdeInfo> build() {
+    ImmutableMap.Builder<Label, RuleIdeInfo> ruleMap = ImmutableMap.builder();
+    for (RuleIdeInfo rule : rules) {
+      ruleMap.put(rule.label, rule);
+    }
+    return ruleMap.build();
+  }
+}
diff --git a/blaze-base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java
new file mode 100644
index 0000000..85c08b7
--- /dev/null
+++ b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/prefetch/MockPrefetchService.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.prefetch;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.intellij.openapi.project.Project;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Mocks the prefetch service.
+ */
+public class MockPrefetchService implements PrefetchService {
+  @Override
+  public ListenableFuture<?> prefetchFiles(List<File> files, boolean synchronous) {
+    return Futures.immediateFuture(null);
+  }
+
+  @Override
+  public void prefetchProjectFiles(Project project) {
+  }
+}
diff --git a/blaze-base/tests/utils/unit/com/google/idea/blaze/base/scope/ErrorCollector.java b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/scope/ErrorCollector.java
new file mode 100644
index 0000000..4b5cb71
--- /dev/null
+++ b/blaze-base/tests/utils/unit/com/google/idea/blaze/base/scope/ErrorCollector.java
@@ -0,0 +1,56 @@
+/*
+ * 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.scope;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Test class that collects issues.
+ */
+public class ErrorCollector implements OutputSink<IssueOutput> {
+  List<IssueOutput> issues = Lists.newArrayList();
+
+  @Override
+  public Propagation onOutput(@NotNull IssueOutput output) {
+    issues.add(output);
+    return Propagation.Continue;
+  }
+
+  public void assertNoIssues() {
+    assertThat(issues).isEmpty();
+  }
+
+  public void assertIssues(@NotNull String... requiredMessages) {
+    List<String> messages = Lists.newArrayList();
+    for (IssueOutput issue : issues) {
+      messages.add(issue.getMessage());
+    }
+    assertThat(messages).containsExactly((Object[]) requiredMessages);
+  }
+
+  public void assertIssueContaining(@NotNull String s) {
+    assertThat(issues.stream().anyMatch((issue) -> issue.getMessage().contains(s)))
+      .named("Issues must contain: " + s)
+      .isTrue();
+  }
+}
+
diff --git a/blaze-cpp/BUILD b/blaze-cpp/BUILD
new file mode 100644
index 0000000..9e7261b
--- /dev/null
+++ b/blaze-cpp/BUILD
@@ -0,0 +1,28 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "blaze-cpp",
+    srcs = glob(["src/**/*.java"]),
+    deps = [
+        "//blaze-base",
+        "//intellij-platform-sdk:plugin_api",
+        "//third_party:jsr305",
+    ],
+)
+
+filegroup(
+    name = "plugin_xml",
+    srcs = ["src/META-INF/blaze-cpp.xml"],
+)
+
+java_library(
+    name = "test_lib",
+    srcs = glob(["tests/**/*.java"]),
+    deps = [
+        ":blaze-cpp",
+        "//blaze-base:unit_test_utils",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
diff --git a/blaze-cpp/src/META-INF/blaze-cpp.xml b/blaze-cpp/src/META-INF/blaze-cpp.xml
new file mode 100644
index 0000000..668e64a
--- /dev/null
+++ b/blaze-cpp/src/META-INF/blaze-cpp.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~ http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+  <depends>com.intellij.modules.cidr.lang</depends>
+  <depends>com.intellij.modules.cidr.debugger</depends>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.cpp.BlazeCSyncPlugin"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="cidr.lang">
+    <languageKindHelper implementation="com.google.idea.blaze.cpp.BlazeLanguageKindCalculatorHelper"/>
+  </extensions>
+  <extensions defaultExtensionNs="com.intellij">
+    <projectService serviceImplementation="com.google.idea.blaze.cpp.BlazeCWorkspace"/>
+  </extensions>
+</idea-plugin>
diff --git a/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
new file mode 100644
index 0000000..75a7297
--- /dev/null
+++ b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCSyncPlugin.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.cpp;
+
+import com.google.common.collect.ImmutableSet;
+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.WorkspaceRoot;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceManager;
+
+import javax.annotation.Nullable;
+import java.util.Set;
+
+public final class BlazeCSyncPlugin extends BlazeSyncPlugin.Adapter {
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.C) {
+      return ImmutableSet.of(LanguageClass.C);
+    }
+    return ImmutableSet.of();
+  }
+
+  @Override
+  public void updateProjectStructure(Project project,
+                                     BlazeContext context,
+                                     WorkspaceRoot workspaceRoot,
+                                     ProjectViewSet projectViewSet,
+                                     BlazeProjectData blazeProjectData,
+                                     @Nullable BlazeProjectData oldBlazeProjectData,
+                                     ModuleEditor moduleEditor,
+                                     Module workspaceModule,
+                                     ModifiableRootModel workspaceModifiableModel) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.C)) {
+      return;
+    }
+
+    Scope.push(context, childContext -> {
+      childContext.push(new TimingScope("Setup C Workspace"));
+
+      OCWorkspace workspace = OCWorkspaceManager.getWorkspace(project);
+      if (workspace instanceof BlazeCWorkspace) {
+        BlazeCWorkspace blazeCWorkspace = (BlazeCWorkspace)workspace;
+        blazeCWorkspace.update(childContext, blazeProjectData);
+      }
+    });
+  }
+}
diff --git a/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
new file mode 100644
index 0000000..2c6f8ef
--- /dev/null
+++ b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCWorkspace.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.idea.blaze.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.metrics.LoggingService;
+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.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.VirtualFile;
+import com.jetbrains.cidr.lang.symbols.OCSymbol;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCWorkspace;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceModificationTrackers;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Main entry point for C/CPP configuration data.
+ */
+public final class BlazeCWorkspace implements OCWorkspace {
+  private static final Logger LOG = Logger.getInstance(BlazeCWorkspace.class);
+
+  @Nullable private final Project project;
+  @Nullable private final OCWorkspaceModificationTrackers modTrackers;
+
+  @Nullable private BlazeConfigurationResolver configurationResolver;
+
+  private BlazeCWorkspace(Project project) {
+    if (Blaze.isBlazeProject(project)) {
+      this.project = project;
+      this.modTrackers = new OCWorkspaceModificationTrackers(project);
+      this.configurationResolver = new BlazeConfigurationResolver(project);
+    } else {
+      this.project = null;
+      this.modTrackers = null;
+    }
+  }
+
+  public static BlazeCWorkspace getInstance(Project project) {
+    return ServiceManager.getService(project, BlazeCWorkspace.class);
+  }
+
+  public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
+    LOG.assertTrue(project != null);
+    LOG.assertTrue(modTrackers != null);
+    LOG.assertTrue(configurationResolver != null);
+
+    long start = System.currentTimeMillis();
+    // Non-incremental update to our c configurations.
+    configurationResolver.update(context, blazeProjectData);
+    long end = System.currentTimeMillis();
+
+    LOG.info(String.format("Blaze OCWorkspace update took: %d ms", (end - start)));
+
+    ApplicationManager.getApplication().runReadAction(() -> {
+      if (project.isDisposed()) {
+        return;
+      }
+
+      // TODO(salguarnieri) Avoid bumping all of these trackers; figure out what has changed.
+      modTrackers.getProjectFilesListTracker().incModificationCount();
+      modTrackers.getSourceFilesListTracker().incModificationCount();
+      modTrackers.getBuildConfigurationChangesTracker().incModificationCount();
+      modTrackers.getBuildSettingsChangesTracker().incModificationCount();
+    });
+  }
+
+  @Override
+  public Collection<VirtualFile> getLibraryFilesToBuildSymbols() {
+  // This method should return all the header files themselves, not the head file directories.
+  // (And not header files in the project; just the ones in the SDK and in any dependencies)
+    return ImmutableList.of();
+  }
+
+  @Override
+  public boolean areFromSameProject(@Nullable VirtualFile a, @Nullable VirtualFile b) {
+    return false;
+  }
+
+  @Override
+  public boolean areFromSamePackage(@Nullable VirtualFile a, @Nullable VirtualFile b) {
+    return false;
+  }
+
+  @Override
+  public boolean isInSDK(@Nullable VirtualFile file) {
+    return false;
+  }
+
+  @Override
+  public boolean isFromWrongSDK(OCSymbol symbol, @Nullable VirtualFile contextFile) {
+    return false;
+  }
+
+  @Nullable
+  @Override
+  public OCResolveConfiguration getSelectedResolveConfiguration() {
+    return null;
+  }
+
+  @Override
+  public OCWorkspaceModificationTrackers getModificationTrackers() {
+    LOG.assertTrue(modTrackers != null);
+    return modTrackers;
+  }
+
+  @Override
+  public List<? extends OCResolveConfiguration> getConfigurations() {
+    return configurationResolver == null ? ImmutableList.of() : configurationResolver.getAllConfigurations();
+  }
+
+  @Override
+  public List<? extends OCResolveConfiguration> getConfigurationsForFile(@Nullable VirtualFile sourceFile) {
+    LoggingService.reportEvent(project, Action.C_RESOLVE_FILE);
+
+    if (sourceFile == null || !sourceFile.isValid() || configurationResolver == null) {
+      return ImmutableList.of();
+    }
+    OCResolveConfiguration config = configurationResolver.getConfigurationForFile(sourceFile);
+    return config == null ? ImmutableList.of() : ImmutableList.of(config);
+  }
+}
+
diff --git a/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java
new file mode 100644
index 0000000..168f691
--- /dev/null
+++ b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCompilerMacros.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.cpp;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiFile;
+import com.jetbrains.cidr.lang.preprocessor.OCInclusionContext;
+import com.jetbrains.cidr.lang.preprocessor.OCInclusionContextUtil;
+import com.jetbrains.cidr.lang.workspace.compiler.CidrCompilerResult;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+import com.jetbrains.cidr.toolchains.CompilerInfoCache;
+
+import java.util.Map;
+
+final class BlazeCompilerMacros extends OCCompilerMacros {
+  private final CompilerInfoCache compilerInfoCache;
+  private final ImmutableList<String> globalDefines;
+  private final ImmutableMap<String, String> globalFeatures;
+  private final OCCompilerSettings compilerSettings;
+  private final Project project;
+
+  public BlazeCompilerMacros(
+    Project project,
+    CompilerInfoCache compilerInfoCache,
+    OCCompilerSettings compilerSettings,
+    ImmutableList<String> defines,
+    ImmutableMap<String, String> features
+  ) {
+    this.project = project;
+    this.compilerInfoCache = compilerInfoCache;
+    this.compilerSettings = compilerSettings;
+    this.globalDefines = defines;
+    this.globalFeatures = features;
+  }
+
+  @Override
+  protected void fillFileMacros(OCInclusionContext context, PsiFile sourceFile) {
+    // Get the default compiler info for this file.
+    VirtualFile vf = OCInclusionContextUtil.getVirtualFile(sourceFile);
+    CidrCompilerResult<CompilerInfoCache.Entry> compilerInfoProvider = compilerInfoCache.getCompilerInfoCache(
+      project,
+      compilerSettings,
+      context.getLanguageKind(),
+      vf
+    );
+    CompilerInfoCache.Entry compilerInfo = compilerInfoProvider.getResult();
+
+    // Combine the info we got from Blaze with the info we get from IntelliJ's methods.
+    UniqueListBuilder<String> allDefinesBuilder = new UniqueListBuilder<>();
+    // IntelliJ expects a string of "#define [VAR_NAME]\n#define [VAR_NAME2]\n..."
+    for (String globalDefine : globalDefines) {
+      allDefinesBuilder.add("#define " + globalDefine);
+    }
+    if (compilerInfo != null) {
+      String[] split = compilerInfo.defines.split("\n");
+      for (String s : split) {
+        allDefinesBuilder.add(s);
+      }
+    }
+    final String allDefines = Joiner.on("\n").join(allDefinesBuilder.build());
+
+    Map<String, String> allFeatures = Maps.newHashMap();
+    allFeatures.putAll(globalFeatures);
+    if (compilerInfo != null) {
+      allFeatures.putAll(compilerInfo.features);
+    }
+
+    fillSubstitutions(context, allDefines);
+    enableClangFeatures(context, allFeatures);
+    enableClangExtensions(context, allFeatures);
+  }
+}
diff --git a/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java
new file mode 100644
index 0000000..e3ba4df
--- /dev/null
+++ b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeCompilerSettings.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
+import com.jetbrains.cidr.lang.toolchains.CidrSwitchBuilder;
+import com.jetbrains.cidr.lang.toolchains.CidrToolEnvironment;
+import com.jetbrains.cidr.lang.toolchains.DefaultCidrToolEnvironment;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerKind;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.List;
+
+final class BlazeCompilerSettings extends OCCompilerSettings {
+  private final CidrToolEnvironment toolEnvironment = new DefaultCidrToolEnvironment();
+
+  private final Project project;
+  @Nullable private final File cCompiler;
+  @Nullable private final File cppCompiler;
+  private final ImmutableList<String> cFlags;
+  private final ImmutableList<String> cppFlags;
+
+  BlazeCompilerSettings(
+    Project project,
+    @Nullable File cCompiler,
+    @Nullable File cppCompiler,
+    ImmutableList<String> cFlags,
+    ImmutableList<String> cppFlags
+  ) {
+    this.project = project;
+    this.cCompiler = cCompiler;
+    this.cppCompiler = cppCompiler;
+    this.cFlags = cFlags;
+    this.cppFlags = cppFlags;
+  }
+
+  @Override
+  public OCCompilerKind getCompiler(OCLanguageKind languageKind) {
+    return null;
+  }
+
+  @Override
+  public File getCompilerExecutable(@NotNull OCLanguageKind lang) {
+    if (lang == OCLanguageKind.C) {
+      return cCompiler;
+    } else if (lang == OCLanguageKind.CPP) {
+      return cppCompiler;
+    }
+    // We don't support objective c/c++.
+    return null;
+  }
+
+  @Override
+  public File getCompilerWorkingDir() {
+    return WorkspaceRoot.fromProject(project).directory();
+  }
+
+  @Override
+  public CidrToolEnvironment getEnvironment() {
+    return toolEnvironment;
+  }
+
+  @Override
+  public CidrCompilerSwitches getCompilerSwitches(OCLanguageKind lang, @Nullable VirtualFile sourceFile) {
+    final List<String> allCompilerFlags;
+    if (lang == OCLanguageKind.C) {
+      allCompilerFlags = cFlags;
+    } else if (lang == OCLanguageKind.CPP) {
+      allCompilerFlags = cppFlags;
+    } else {
+      allCompilerFlags = ImmutableList.of();
+    }
+
+    CidrSwitchBuilder builder = new CidrSwitchBuilder();
+    // Because there can be both escaped and unescaped spaces in the flag, first unescape the spaces and then escape all of them.
+    allCompilerFlags.stream()
+      .map(flag -> flag.replace("\\ ", " "))
+      .map(flag -> flag.replace(" ", "\\ "))
+      .forEach(builder::addSingle);
+
+    return builder.build();
+  }
+}
+
diff --git a/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
new file mode 100644
index 0000000..a6deb7e
--- /dev/null
+++ b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeConfigurationResolver.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.cpp;
+
+import com.google.common.collect.*;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.rulemaps.SourceToRuleMap;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.ScopedFunction;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+
+final class BlazeConfigurationResolver {
+  private static final class MapEntry {
+    public final Label label;
+    public final BlazeResolveConfiguration configuration;
+
+    public MapEntry(Label label, BlazeResolveConfiguration configuration) {
+      this.label = label;
+      this.configuration = configuration;
+    }
+  }
+
+  private static final Logger LOG = Logger.getInstance(BlazeConfigurationResolver.class);
+  private final Project project;
+
+  private ImmutableMap<Label, BlazeResolveConfiguration> resolveConfigurations = ImmutableMap.of();
+
+  public BlazeConfigurationResolver(Project project) {
+    this.project = project;
+  }
+
+  public void update(BlazeContext context, BlazeProjectData blazeProjectData) {
+    WorkspacePathResolver workspacePathResolver = blazeProjectData.workspacePathResolver;
+    ImmutableMap<Label, CToolchainIdeInfo> toolchainLookupMap = BlazeResolveConfiguration.buildToolchainLookupMap(
+      context,
+      blazeProjectData.ruleMap,
+      blazeProjectData.reverseDependencies
+    );
+    resolveConfigurations = buildBlazeConfigurationMap(context, blazeProjectData, toolchainLookupMap, workspacePathResolver);
+  }
+
+  private ImmutableMap<Label, BlazeResolveConfiguration> buildBlazeConfigurationMap(
+    BlazeContext parentContext,
+    BlazeProjectData blazeProjectData,
+    ImmutableMap<Label, CToolchainIdeInfo> toolchainLookupMap,
+    WorkspacePathResolver workspacePathResolver
+  ) {
+    // Type specification needed to avoid incorrect type inference during command line build.
+    return Scope.push(parentContext, (ScopedFunction<ImmutableMap<Label, BlazeResolveConfiguration>>)context -> {
+      context.push(new TimingScope("Build C configuration map"));
+
+      ConcurrentMap<CToolchainIdeInfo, File> compilerWrapperCache = Maps.newConcurrentMap();
+      List<ListenableFuture<MapEntry>> mapEntryFutures = Lists.newArrayList();
+
+      for (RuleIdeInfo rule : blazeProjectData.ruleMap.values()) {
+        if (rule.kind.getLanguageClass() == LanguageClass.C) {
+          ListenableFuture<MapEntry> future =
+            submit(
+              () -> createResolveConfiguration(
+                rule,
+                toolchainLookupMap,
+                compilerWrapperCache,
+                workspacePathResolver,
+                blazeProjectData)
+            );
+          mapEntryFutures.add(future);
+        }
+      }
+
+      ImmutableMap.Builder<Label, BlazeResolveConfiguration> newResolveConfigurations = ImmutableMap.builder();
+      List<MapEntry> mapEntries;
+      try {
+        mapEntries = Futures.allAsList(mapEntryFutures).get();
+      }
+      catch (InterruptedException | ExecutionException e) {
+        Thread.currentThread().interrupt();
+        LOG.warn("Could not build C resolve configurations", e);
+        context.setCancelled();
+        return ImmutableMap.of();
+      }
+
+      for (MapEntry mapEntry : mapEntries) {
+        // Skip over labels that don't have C configuration data.
+        if (mapEntry != null) {
+          newResolveConfigurations.put(mapEntry.label, mapEntry.configuration);
+        }
+      }
+      return newResolveConfigurations.build();
+    });
+  }
+
+  private static ListenableFuture<MapEntry> submit(Callable<MapEntry> callable) {
+    return BlazeExecutor.getInstance().submit(callable);
+  }
+
+  @Nullable
+  private MapEntry createResolveConfiguration(
+    RuleIdeInfo rule,
+    ImmutableMap<Label, CToolchainIdeInfo> toolchainLookupMap,
+    ConcurrentMap<CToolchainIdeInfo, File> compilerWrapperCache,
+    WorkspacePathResolver workspacePathResolver,
+    BlazeProjectData blazeProjectData
+  ) {
+    Label label = rule.label;
+    LOG.info("Resolving " + label.toString());
+    CToolchainIdeInfo toolchainIdeInfo = toolchainLookupMap.get(label);
+    if (toolchainIdeInfo != null) {
+      File compilerWrapper = findOrCreateCompilerWrapperScript(
+        compilerWrapperCache,
+        toolchainIdeInfo,
+        workspacePathResolver,
+        rule.label
+      );
+      if (compilerWrapper != null) {
+        BlazeResolveConfiguration config = BlazeResolveConfiguration.createConfigurationForTarget(
+          project,
+          workspacePathResolver,
+          blazeProjectData.ruleMap.get(label),
+          toolchainIdeInfo,
+          compilerWrapper
+        );
+        if (config != null) {
+          return new MapEntry(label, config);
+        }
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  private static File findOrCreateCompilerWrapperScript(
+    Map<CToolchainIdeInfo, File> compilerWrapperCache,
+    CToolchainIdeInfo toolchainIdeInfo,
+    WorkspacePathResolver workspacePathResolver,
+    Label label
+  ) {
+    File compilerWrapper = compilerWrapperCache.get(toolchainIdeInfo);
+    if (compilerWrapper == null) {
+      File cppExecutable = workspacePathResolver.resolveToFile(toolchainIdeInfo.cppExecutable.getAbsoluteOrRelativeFile().getPath());
+      if (cppExecutable == null) {
+        String errorMessage = String.format(
+          "Unable to find compiler executable: %s for rule %s",
+          toolchainIdeInfo.cppExecutable.toString(),
+          label.toString());
+        LOG.warn(errorMessage);
+        compilerWrapper = null;
+      } else {
+        compilerWrapper = createCompilerExecutableWrapper(cppExecutable);
+        if (compilerWrapper != null) {
+          compilerWrapperCache.put(toolchainIdeInfo, compilerWrapper);
+        }
+      }
+    }
+    return compilerWrapper;
+  }
+
+  /**
+   * Create a wrapper script that transforms the CLion compiler invocation into a safe invocation of the compiler script that blaze uses.
+   *
+   * CLion passes arguments to the compiler in an arguments file. The c toolchain compiler wrapper script doesn't handle arguments files, so
+   * we need to move the compiler arguments from the file to the command line.
+   *
+   * @param blazeCompilerExecutableFile blaze compiler wrapper
+   * @return The wrapper script that CLion can call.
+   */
+  @Nullable
+  private static File createCompilerExecutableWrapper(File blazeCompilerExecutableFile) {
+    try {
+      File blazeCompilerWrapper = FileUtil.createTempFile("blaze_compiler", ".sh", true /* deleteOnExit */);
+      if (!blazeCompilerWrapper.setExecutable(true)) {
+        return null;
+      }
+      ImmutableList<String> COMPILER_WRAPPER_SCRIPT_LINES = ImmutableList.of(
+        "#!/bin/bash",
+        "",
+        "# The c toolchain compiler wrapper script doesn't handle arguments files, so we",
+        "# need to move the compiler arguments from the file to the command line.",
+        "",
+        "if [ $# -ne 2 ]; then",
+        "  echo \"Usage: $0 @arg-file compile-file\"",
+        "  exit 2;",
+        "fi",
+        "",
+        "if [[ $1 != @* ]]; then",
+        "  echo \"Usage: $0 @arg-file compile-file\"",
+        "  exit 3;",
+        "fi",
+        "",
+        " # Remove the @ before the arguments file path",
+        "ARG_FILE=${1#@}",
+        "# The actual compiler wrapper script we get from blaze",
+        "EXE=" + blazeCompilerExecutableFile.getPath(),
+        "# Read in the arguments file so we can pass the arguments on the command line.",
+        "ARGS=`cat $ARG_FILE`",
+        "$EXE $ARGS $2"
+      );
+
+      try (PrintWriter pw = new PrintWriter(blazeCompilerWrapper)) {
+        COMPILER_WRAPPER_SCRIPT_LINES.forEach(pw::println);
+      }
+      return blazeCompilerWrapper;
+    }
+    catch (IOException e) {
+      return null;
+    }
+  }
+
+  @Nullable
+  public OCResolveConfiguration getConfigurationForFile(VirtualFile sourceFile) {
+    SourceToRuleMap sourceToRuleMap = SourceToRuleMap.getInstance(project);
+    List<Label> targetsForSourceFile =
+      Lists.newArrayList(sourceToRuleMap.getTargetsForSourceFile(VfsUtilCore.virtualToIoFile(sourceFile)));
+    if (targetsForSourceFile.isEmpty()) {
+      return null;
+    }
+
+    // If a source file is in two different targets, we can't possibly show how it will be interpreted in both contexts at the same time
+    // in the IDE, so just pick the first target after we sort.
+    targetsForSourceFile.sort((o1, o2) -> o1.toString().compareTo(o2.toString()));
+    Label target = Iterables.getFirst(targetsForSourceFile, null);
+    assert(target != null);
+
+    return resolveConfigurations.get(target);
+  }
+
+  public List<? extends OCResolveConfiguration> getAllConfigurations() {
+    return ImmutableList.copyOf(resolveConfigurations.values());
+  }
+}
diff --git a/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeLanguageKindCalculatorHelper.java b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeLanguageKindCalculatorHelper.java
new file mode 100644
index 0000000..a3f4b91
--- /dev/null
+++ b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeLanguageKindCalculatorHelper.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.cpp;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.workspace.OCLanguageKindCalculatorHelper;
+
+import javax.annotation.Nullable;
+
+public final class BlazeLanguageKindCalculatorHelper implements OCLanguageKindCalculatorHelper {
+  @Nullable
+  @Override
+  public OCLanguageKind getSpecifiedLanguage(Project project, VirtualFile file) {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public OCLanguageKind getLanguageByExtension(Project project, String name) {
+    if (Blaze.isBlazeProject(project)) {
+      String extension = FileUtilRt.getExtension(name);
+      if (extension.equalsIgnoreCase("c")) {
+        return OCLanguageKind.C;
+      }
+      if (extension.equalsIgnoreCase("cc")) {
+        return OCLanguageKind.CPP;
+      }
+    }
+    return null;
+  }
+}
diff --git a/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java
new file mode 100644
index 0000000..06c3f0f
--- /dev/null
+++ b/blaze-cpp/src/com/google/idea/blaze/cpp/BlazeResolveConfiguration.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.cpp;
+
+import com.google.common.collect.*;
+import com.google.idea.blaze.base.ideinfo.CRuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.CToolchainIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.UserDataHolderBase;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.jetbrains.cidr.lang.OCFileTypeHelpers;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.preprocessor.OCImportGraph;
+import com.jetbrains.cidr.lang.workspace.OCLanguageKindCalculator;
+import com.jetbrains.cidr.lang.workspace.OCResolveConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCResolveRootAndConfiguration;
+import com.jetbrains.cidr.lang.workspace.OCWorkspaceUtil;
+import com.jetbrains.cidr.lang.workspace.compiler.CidrCompilerResult;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerMacros;
+import com.jetbrains.cidr.lang.workspace.compiler.OCCompilerSettings;
+import com.jetbrains.cidr.lang.workspace.headerRoots.HeaderRoots;
+import com.jetbrains.cidr.lang.workspace.headerRoots.HeadersSearchRoot;
+import com.jetbrains.cidr.lang.workspace.headerRoots.IncludedHeadersRoot;
+import com.jetbrains.cidr.toolchains.CompilerInfoCache;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+
+public final class BlazeResolveConfiguration extends UserDataHolderBase implements OCResolveConfiguration {
+  public static final Logger LOG = Logger.getInstance(BlazeResolveConfiguration.class);
+
+  private final WorkspacePathResolver workspacePathResolver;
+
+  private final Project project;
+  private final Label label;
+
+  private final ImmutableList<HeadersSearchRoot> cLibraryIncludeRoots;
+  private final ImmutableList<HeadersSearchRoot> cppLibraryIncludeRoots;
+  private final HeaderRoots projectIncludeRoots;
+
+  private final CompilerInfoCache compilerInfoCache;
+  private final BlazeCompilerMacros compilerMacros;
+  private final BlazeCompilerSettings compilerSettings;
+
+  @Nullable
+  public static BlazeResolveConfiguration createConfigurationForTarget(
+    Project project,
+    WorkspacePathResolver workspacePathResolver,
+    RuleIdeInfo ruleIdeInfo,
+    CToolchainIdeInfo toolchainIdeInfo,
+    File compilerWrapper
+  ) {
+    CRuleIdeInfo cRuleIdeInfo = ruleIdeInfo.cRuleIdeInfo;
+    if (cRuleIdeInfo == null) {
+      return null;
+    }
+
+    UniqueListBuilder<ExecutionRootPath> systemIncludesBuilder = new UniqueListBuilder<>();
+    systemIncludesBuilder.addAll(cRuleIdeInfo.transitiveSystemIncludeDirectories);
+    systemIncludesBuilder.addAll(toolchainIdeInfo.builtInIncludeDirectories);
+    systemIncludesBuilder.addAll(toolchainIdeInfo.unfilteredToolchainSystemIncludes);
+
+    UniqueListBuilder<ExecutionRootPath> userIncludesBuilder = new UniqueListBuilder<>();
+    userIncludesBuilder.addAll(cRuleIdeInfo.transitiveIncludeDirectories);
+
+    UniqueListBuilder<ExecutionRootPath> userQuoteIncludesBuilder = new UniqueListBuilder<>();
+    userQuoteIncludesBuilder.addAll(cRuleIdeInfo.transitiveQuoteIncludeDirectories);
+
+    ImmutableList.Builder<String> cFlagsBuilder = ImmutableList.builder();
+    cFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
+    cFlagsBuilder.addAll(toolchainIdeInfo.cCompilerOptions);
+    cFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
+
+    ImmutableList.Builder<String> cppFlagsBuilder = ImmutableList.builder();
+    cppFlagsBuilder.addAll(toolchainIdeInfo.baseCompilerOptions);
+    cppFlagsBuilder.addAll(toolchainIdeInfo.cppCompilerOptions);
+    cppFlagsBuilder.addAll(toolchainIdeInfo.unfilteredCompilerOptions);
+
+    ImmutableMap<String, String> features = ImmutableMap.of();
+
+    return new BlazeResolveConfiguration(
+      project,
+      workspacePathResolver,
+      ruleIdeInfo.label,
+      systemIncludesBuilder.build(),
+      systemIncludesBuilder.build(),
+      userQuoteIncludesBuilder.build(),
+      userIncludesBuilder.build(),
+      userIncludesBuilder.build(),
+      cRuleIdeInfo.transitiveDefines,
+      features,
+      compilerWrapper,
+      compilerWrapper,
+      cFlagsBuilder.build(),
+      cppFlagsBuilder.build()
+    );
+  }
+
+  public static ImmutableMap<Label, CToolchainIdeInfo> buildToolchainLookupMap(
+    BlazeContext context,
+    ImmutableMap<Label, RuleIdeInfo> ruleMap,
+    ImmutableMultimap<Label, Label> reverseDependencies
+  ) {
+    return Scope.push(context, childContext -> {
+      childContext.push(new TimingScope("Build toolchain lookup map"));
+
+      List<Label> seeds = Lists.newArrayList();
+      for (Map.Entry<Label, RuleIdeInfo> entry : ruleMap.entrySet()) {
+        CToolchainIdeInfo cToolchainIdeInfo = entry.getValue().cToolchainIdeInfo;
+        if (cToolchainIdeInfo != null) {
+          seeds.add(entry.getKey());
+        }
+      }
+
+      Map<Label, CToolchainIdeInfo> lookupTable = Maps.newHashMap();
+      for (Label seed : seeds) {
+        CToolchainIdeInfo toolchainInfo = ruleMap.get(seed).cToolchainIdeInfo;
+        LOG.assertTrue(toolchainInfo != null);
+        List<Label> worklist = Lists.newArrayList(reverseDependencies.get(seed));
+        while (!worklist.isEmpty()) {
+          // We should never see a label depend on two different toolchains.
+          Label l = worklist.remove(0);
+          CToolchainIdeInfo previousValue = lookupTable.putIfAbsent(l, toolchainInfo);
+          // Don't propagate the toolchain twice.
+          if (previousValue == null) {
+            worklist.addAll(reverseDependencies.get(l));
+          }
+          else {
+            LOG.assertTrue(previousValue.equals(toolchainInfo));
+          }
+        }
+      }
+      return ImmutableMap.copyOf(lookupTable);
+    });
+  }
+
+  public BlazeResolveConfiguration(
+    Project project,
+    WorkspacePathResolver workspacePathResolver,
+    Label label,
+    ImmutableList<ExecutionRootPath> cSystemIncludeDirs,
+    ImmutableList<ExecutionRootPath> cppSystemIncludeDirs,
+    ImmutableList<ExecutionRootPath> quoteIncludeDirs,
+    ImmutableList<ExecutionRootPath> cIncludeDirs,
+    ImmutableList<ExecutionRootPath> cppIncludeDirs,
+    ImmutableList<String> defines,
+    ImmutableMap<String, String> features,
+    File cCompilerExecutable,
+    File cppCompilerExecutable,
+    ImmutableList<String> cCompilerFlags,
+    ImmutableList<String> cppCompilerFlags
+  ) {
+    this.workspacePathResolver = workspacePathResolver;
+    this.project = project;
+    this.label = label;
+
+    ImmutableList.Builder<HeadersSearchRoot> cIncludeRootsBuilder = ImmutableList.builder();
+    collectHeaderRoots(cIncludeRootsBuilder, cIncludeDirs, true /* isUserHeader */);
+    collectHeaderRoots(cIncludeRootsBuilder, cSystemIncludeDirs, false /* isUserHeader */);
+    this.cLibraryIncludeRoots = cIncludeRootsBuilder.build();
+
+    ImmutableList.Builder<HeadersSearchRoot> cppIncludeRootsBuilder = ImmutableList.builder();
+    collectHeaderRoots(cppIncludeRootsBuilder, cppIncludeDirs, true /* isUserHeader */);
+    collectHeaderRoots(cppIncludeRootsBuilder, cppSystemIncludeDirs, false /* isUserHeader */);
+    this.cppLibraryIncludeRoots = cppIncludeRootsBuilder.build();
+
+    ImmutableList.Builder<HeadersSearchRoot> quoteIncludeRootsBuilder = ImmutableList.builder();
+    collectHeaderRoots(quoteIncludeRootsBuilder, quoteIncludeDirs, true /* isUserHeader */);
+    this.projectIncludeRoots = new HeaderRoots(quoteIncludeRootsBuilder.build());
+
+    this.compilerSettings = new BlazeCompilerSettings(
+      project,
+      cCompilerExecutable,
+      cppCompilerExecutable,
+      cCompilerFlags,
+      cppCompilerFlags
+    );
+
+    this.compilerInfoCache = new CompilerInfoCache();
+    this.compilerMacros = new BlazeCompilerMacros(
+      project,
+      compilerInfoCache,
+      compilerSettings,
+      defines,
+      features
+    );
+  }
+
+  @Override
+  public Project getProject() {
+    return project;
+  }
+
+  @Override
+  public String getDisplayName(boolean shorten) {
+    return label.toString();
+  }
+
+  @Nullable
+  @Override
+  public VirtualFile getPrecompiledHeader() {
+    return null;
+  }
+
+  @Nullable
+  @Override
+  public OCLanguageKind getDeclaredLanguageKind(VirtualFile sourceOrHeaderFile) {
+    String fileName = sourceOrHeaderFile.getName();
+    if (OCFileTypeHelpers.isSourceFile(fileName)) {
+      return getLanguageKind(sourceOrHeaderFile);
+    }
+
+    if (OCFileTypeHelpers.isHeaderFile(fileName)) {
+      return getLanguageKind(getSourceFileForHeaderFile(sourceOrHeaderFile));
+    }
+
+    return null;
+  }
+
+  private OCLanguageKind getLanguageKind(@Nullable VirtualFile sourceFile) {
+    OCLanguageKind kind = OCLanguageKindCalculator.tryFileTypeAndExtension(project, sourceFile);
+    return kind != null ? kind : getMaximumLanguageKind();
+  }
+
+  @Nullable
+  private VirtualFile getSourceFileForHeaderFile(VirtualFile headerFile) {
+    ArrayList<VirtualFile> roots = new ArrayList<>(OCImportGraph.getAllHeaderRoots(project, headerFile));
+
+    final String headerNameWithoutExtension = headerFile.getNameWithoutExtension();
+    for (VirtualFile root : roots) {
+      if (root.getNameWithoutExtension().equals(headerNameWithoutExtension)) {
+        return root;
+      }
+    }
+    return null;
+  }
+
+  @Override
+  public OCLanguageKind getPrecompiledLanguageKind() {
+    return getMaximumLanguageKind();
+  }
+
+  @Override
+  public OCLanguageKind getMaximumLanguageKind() {
+    return OCLanguageKind.CPP;
+  }
+
+  @Override
+  public HeaderRoots getProjectHeadersRoots() {
+    return projectIncludeRoots;
+  }
+
+  @Override
+  public HeaderRoots getLibraryHeadersRoots(OCResolveRootAndConfiguration headerContext) {
+    OCLanguageKind languageKind = headerContext.getKind();
+    VirtualFile sourceFile = headerContext.getRootFile();
+    if (languageKind == null) {
+      languageKind = getLanguageKind(sourceFile);
+    }
+
+    UniqueListBuilder<HeadersSearchRoot> roots = new UniqueListBuilder<>();
+    if (languageKind == OCLanguageKind.C) {
+      roots.addAll(cLibraryIncludeRoots);
+    } else {
+      roots.addAll(cppLibraryIncludeRoots);
+    }
+
+    CidrCompilerResult<CompilerInfoCache.Entry> compilerInfoCacheHolder = compilerInfoCache.getCompilerInfoCache(
+      project,
+      compilerSettings,
+      languageKind,
+      sourceFile
+    );
+    CompilerInfoCache.Entry compilerInfo = compilerInfoCacheHolder.getResult();
+    if (compilerInfo != null) {
+      roots.addAll(compilerInfo.headerSearchPaths);
+    }
+
+    return new HeaderRoots(roots.build());
+  }
+
+  private void collectHeaderRoots(
+    ImmutableList.Builder<HeadersSearchRoot> roots,
+    ImmutableList<ExecutionRootPath> paths,
+    boolean isUserHeader
+  ) {
+    for (ExecutionRootPath executionRootPath : paths) {
+      ImmutableList<File> possibleDirectories = workspacePathResolver.resolveToIncludeDirectories(executionRootPath);
+      for (File f : possibleDirectories) {
+        VirtualFile vf = getVirtualFile(f);
+        if (vf == null) {
+          LOG.debug(String.format("Header root %s could not be converted to a virtual file", f.getAbsolutePath()));
+        }
+        else {
+          roots.add(new IncludedHeadersRoot(project, vf, false /* recursive */, isUserHeader));
+        }
+      }
+    }
+  }
+
+  @Nullable
+  private static VirtualFile getVirtualFile(File file) {
+    // Fail fast if the file doesn't even exist.
+    if (!file.exists()) {
+      return null;
+    }
+    return VfsUtil.findFileByIoFile(file, true);
+  }
+
+  @Override
+  public OCCompilerMacros getCompilerMacros() {
+    return compilerMacros;
+  }
+
+  @Override
+  public OCCompilerSettings getCompilerSettings() {
+    return compilerSettings;
+  }
+
+  @Nullable
+  @Override
+  public Object getIndexingCluster() {
+    return null;
+  }
+
+  @Override
+  public int compareTo(OCResolveConfiguration other) {
+    return OCWorkspaceUtil.compareConfigurations(this, other);
+  }
+
+  @Override
+  public int hashCode() {
+    // There should only be one configuration per label.
+    return Objects.hash(label);
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+
+    if (!(obj instanceof BlazeResolveConfiguration)) {
+      return false;
+    }
+
+    BlazeResolveConfiguration that = (BlazeResolveConfiguration)obj;
+    return compareTo(that) == 0;
+  }
+}
+
diff --git a/blaze-cpp/src/com/google/idea/blaze/cpp/UniqueListBuilder.java b/blaze-cpp/src/com/google/idea/blaze/cpp/UniqueListBuilder.java
new file mode 100644
index 0000000..a9ebfba
--- /dev/null
+++ b/blaze-cpp/src/com/google/idea/blaze/cpp/UniqueListBuilder.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.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+/**
+ * A list where an item is only added if it is not already in the list.
+ */
+final class UniqueListBuilder<T> {
+  private final Set<T> set = Sets.newLinkedHashSet();
+
+  /**
+   * Add {@param element} if it is not already in the list.
+   * @return true if the element has been added, false otherwise.
+   */
+  public boolean add(T element) {
+    return set.add(element);
+  }
+
+  /**
+   * For each element in {@param elements} add the element to the list if it is not already in the list.
+   */
+  public void addAll(Iterable<T> elements) {
+    for (T element : elements) {
+      add(element);
+    }
+  }
+
+  public ImmutableList<T> build() {
+    return ImmutableList.copyOf(set);
+  }
+}
diff --git a/blaze-cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java b/blaze-cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java
new file mode 100644
index 0000000..fe69893
--- /dev/null
+++ b/blaze-cpp/tests/unittests/com/google/idea/blaze/cpp/BlazeCompilerSettingsTest.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.cpp;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.jetbrains.cidr.lang.OCLanguageKind;
+import com.jetbrains.cidr.lang.toolchains.CidrCompilerSwitches;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+public class BlazeCompilerSettingsTest extends BlazeTestCase {
+  @Test
+  public void testCompilerSwitchesSimple() {
+    File cppExe = new File("bin/cpp");
+    ImmutableList<String> cFlags = ImmutableList.of("-fast", "-slow");
+    BlazeCompilerSettings settings = new BlazeCompilerSettings(
+      getProject(),
+      cppExe,
+      cppExe,
+      cFlags,
+      cFlags
+    );
+
+    CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
+    List<String> commandLineArgs = compilerSwitches.getCommandLineArgs();
+    assertThat(commandLineArgs).containsExactly("-fast", "-slow");
+  }
+
+  @Test
+  public void testCompilerSwitchesWithUnescapedSpaces() {
+    File cppExe = new File("bin/cpp");
+    ImmutableList<String> cFlags = ImmutableList.of("-f ast", "-slo w");
+    BlazeCompilerSettings settings = new BlazeCompilerSettings(
+      getProject(),
+      cppExe,
+      cppExe,
+      cFlags,
+      cFlags
+    );
+
+    CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
+    List<String> commandLineArgs = compilerSwitches.getCommandLineArgs();
+    assertThat(commandLineArgs).containsExactly("-f\\ ast", "-slo\\ w");
+  }
+
+  @Test
+  public void testCompilerSwitchesWithEscapedSpaces() {
+    File cppExe = new File("bin/cpp");
+    ImmutableList<String> cFlags = ImmutableList.of("-f\\ ast", "-slo\\ w");
+    BlazeCompilerSettings settings = new BlazeCompilerSettings(
+      getProject(),
+      cppExe,
+      cppExe,
+      cFlags,
+      cFlags
+    );
+
+    CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
+    List<String> commandLineArgs = compilerSwitches.getCommandLineArgs();
+    assertThat(commandLineArgs).containsExactly("-f\\ ast", "-slo\\ w");
+  }
+
+  @Test
+  public void testCompilerSwitchesWithUnescapedAndEscapedSpaces() {
+    File cppExe = new File("bin/cpp");
+    ImmutableList<String> cFlags = ImmutableList.of("-f ast", "-slo\\ w");
+    BlazeCompilerSettings settings = new BlazeCompilerSettings(
+      getProject(),
+      cppExe,
+      cppExe,
+      cFlags,
+      cFlags
+    );
+
+    CidrCompilerSwitches compilerSwitches = settings.getCompilerSwitches(OCLanguageKind.C, null);
+    List<String> commandLineArgs = compilerSwitches.getCommandLineArgs();
+    assertThat(commandLineArgs).containsExactly("-f\\ ast", "-slo\\ w");
+  }
+}
diff --git a/blaze-java/BUILD b/blaze-java/BUILD
new file mode 100644
index 0000000..fa0a1e6
--- /dev/null
+++ b/blaze-java/BUILD
@@ -0,0 +1,57 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "blaze-java",
+    srcs = glob(["src/**/*.java"]),
+    deps = [
+        "//blaze-base",
+        "//blaze-base:proto-deps",
+        "//intellij-platform-sdk:plugin_api",
+        "//third_party:jsr305",
+    ],
+)
+
+filegroup(
+    name = "plugin_xml",
+    srcs = ["src/META-INF/blaze-java.xml"],
+)
+
+load(
+    "//intellij_test:test_defs.bzl",
+    "intellij_test",
+)
+
+intellij_test(
+    name = "unit_tests",
+    srcs = glob(["tests/unittests/**/*.java"]),
+    test_package_root = "com.google.idea.blaze.java",
+    deps = [
+        ":blaze-java",
+        "//blaze-base",
+        "//blaze-base:proto-deps",
+        "//blaze-base:unit_test_utils",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//intellij_test:lib",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
+
+intellij_test(
+    name = "integration_tests",
+    srcs = glob(["tests/integrationtests/**/*.java"]),
+    integration_tests = True,
+    required_plugins = "com.google.idea.blaze.ijwb",
+    test_package_root = "com.google.idea.blaze.java",
+    deps = [
+        ":blaze-java",
+        "//blaze-base",
+        "//blaze-base:integration_test_utils",
+        "//blaze-base:unit_test_utils",
+        "//ijwb:ijwb_bazel",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//intellij_test:lib",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
diff --git a/blaze-java/src/META-INF/blaze-java.xml b/blaze-java/src/META-INF/blaze-java.xml
new file mode 100644
index 0000000..132125d
--- /dev/null
+++ b/blaze-java/src/META-INF/blaze-java.xml
@@ -0,0 +1,91 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+  <depends>com.intellij.modules.java</depends>
+  <depends>JUnit</depends>
+
+  <actions>
+    <action class="com.google.idea.blaze.java.libraries.ExcludeLibraryAction"
+            id="Blaze.ExcludeLibraryAction"
+            icon="BlazeIcons.Blaze"
+            text="Exclude Library and Resync">
+      <add-to-group group-id="Blaze.ProjectViewPopupMenu"/>
+    </action>
+    <action class="com.google.idea.blaze.java.libraries.AttachSourceJarAction"
+            id="Blaze.AttachSourceJarAction"
+            icon="BlazeIcons.Blaze"
+            text="Attach Source Jar">
+      <add-to-group group-id="Blaze.ProjectViewPopupMenu"/>
+    </action>
+
+    <!-- IntelliJ specific actions -->
+
+    <action id="Blaze.ImportProject2" class="com.google.idea.blaze.java.wizard2.BlazeImportProjectAction" icon="BlazeIcons.Blaze">
+      <add-to-group group-id="WelcomeScreen.QuickStart" />
+      <add-to-group group-id="OpenProjectGroup" relative-to-action="ImportProject" anchor="after"/>
+    </action>
+
+    <action id="Blaze.ImportProject" class="com.google.idea.blaze.java.wizard.BlazeImportNewJavaProjectAction" text="Import Blaze Project (old)..." icon="BlazeIcons.Blaze">
+      <add-to-group group-id="WelcomeScreen.QuickStart" />
+      <add-to-group group-id="OpenProjectGroup" relative-to-action="ImportProject" anchor="after"/>
+    </action>
+
+    <!-- End IntelliJ specific actions -->
+
+  </actions>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.java.sync.BlazeJavaSyncPlugin"/>
+    <PsiFileProvider implementation="com.google.idea.blaze.java.psi.JavaPsiFileProvider" />
+  </extensions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaTestClassConfigurationProducer"
+        order="first"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.BlazeJavaTestMethodConfigurationProducer"
+        order="first"/>
+    <projectViewNodeDecorator implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusClassNodeDecorator"/>
+    <editorTabColorProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabColorProvider"/>
+    <editorTabTitleProvider implementation="com.google.idea.blaze.java.syncstatus.BlazeJavaSyncStatusEditorTabTitleProvider"/>
+    <applicationService serviceInterface="com.google.idea.blaze.java.sync.source.JavaSourcePackageReader"
+                        serviceImplementation="com.google.idea.blaze.java.sync.source.JavaSourcePackageReader"/>
+    <applicationService serviceInterface="com.google.idea.blaze.java.sync.source.PackageManifestReader"
+                        serviceImplementation="com.google.idea.blaze.java.sync.source.PackageManifestReader"/>
+    <runConfigurationProducer
+        implementation="com.google.idea.blaze.java.run.producers.AllInPackageBlazeConfigurationProducer"
+        order="first"/>
+    <configurationType implementation="com.google.idea.blaze.java.run.BlazeCommandRunConfigurationType"/>
+    <programRunner implementation="com.google.idea.blaze.java.run.BlazeCommandDebuggerRunner"/>
+    <projectService serviceInterface="com.google.idea.blaze.base.ui.BlazeProblemsView"
+                    serviceImplementation="com.google.idea.blaze.java.ui.BlazeIntelliJProblemsView"/>
+    <projectService serviceImplementation="com.google.idea.blaze.java.libraries.SourceJarManager"/>
+    <refactoring.safeDeleteProcessor id="build_file_safe_delete" order="before javaProcessor"
+                                     implementation="com.google.idea.blaze.java.lang.build.BuildFileSafeDeleteProcessor"/>
+
+    <!-- IntelliJ specific extension points -->
+    <projectImportProvider implementation="com.google.idea.blaze.java.wizard.BlazeNewProjectImportProvider"/>
+    <projectImportBuilder implementation="com.google.idea.blaze.java.wizard.BlazeNewJavaProjectImportBuilder"/>
+    <attachSourcesProvider implementation="com.google.idea.blaze.java.libraries.BlazeAttachSourceProvider"/>
+    <!-- End IntelliJ specific extension points -->
+  </extensions>
+
+  <extensionPoints>
+    <extensionPoint qualifiedName="com.google.idea.blaze.java.JavaSyncAugmenter"
+                    interface="com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter"/>
+  </extensionPoints>
+</idea-plugin>
diff --git a/blaze-java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java b/blaze-java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java
new file mode 100644
index 0000000..7fe5066
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/lang/build/BuildFileSafeDeleteProcessor.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.lang.build;
+
+import com.google.idea.blaze.base.lang.buildfile.references.GlobReference;
+import com.google.idea.blaze.base.lang.buildfile.search.BlazePackage;
+import com.google.idea.blaze.base.lang.buildfile.search.ResolveUtil;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFileSystemItem;
+import com.intellij.refactoring.safeDelete.JavaSafeDeleteProcessor;
+import com.intellij.refactoring.safeDelete.NonCodeUsageSearchInfo;
+import com.intellij.refactoring.safeDelete.usageInfo.SafeDeleteUsageInfo;
+import com.intellij.usageView.UsageInfo;
+import com.intellij.util.IncorrectOperationException;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Removes glob references which don't refer directly to the item(s) being deleted (b/28979434)
+ * (e.g. indirect references from glob([*.java])).<p>
+ *
+ * Runs before JavaSafeDeleteProcessor, and delegates to it, removing indirect glob references.
+ * Only the first valid SafeDeleteProcessorDelegate is used in almost all cases*, so this class effectively
+ * replaces JavaSafeDeleteProcessor (*in the situations where all processors are used, this class has no effect).
+ */
+public class BuildFileSafeDeleteProcessor extends JavaSafeDeleteProcessor {
+
+  /**
+   * Delegates to JavaSafeDeleteProcessor, then removes indirect glob references which we don't want to block safe delete.
+   */
+  @Nullable
+  @Override
+  public NonCodeUsageSearchInfo findUsages(@NotNull PsiElement element,
+                                           @NotNull PsiElement[] allElementsToDelete,
+                                           @NotNull List<UsageInfo> result) {
+    NonCodeUsageSearchInfo superResult = super.findUsages(element, allElementsToDelete, result);
+    Iterator<UsageInfo> iter = result.iterator();
+    while (iter.hasNext()) {
+      if (ignoreUsage(iter.next())) {
+        iter.remove();
+      }
+    }
+    return superResult;
+  }
+
+  /**
+   * We keep globs which reference the file directly (i.e. without wildcards), and remove all
+   * indirect references for the purposes of the 'safe delete' action.
+   */
+  private static boolean ignoreUsage(UsageInfo usage) {
+    if (usage.getReference() instanceof GlobReference && usage instanceof SafeDeleteUsageInfo) {
+      PsiElement referencedElement = ((SafeDeleteUsageInfo) usage).getReferencedElement();
+      PsiFileSystemItem file = ResolveUtil.asFileSystemItemSearch(referencedElement);
+      String relativePath = getBlazePackageRelativePathToFile(file);
+      if (relativePath == null) {
+        return false;
+      }
+      return !((GlobReference) usage.getReference()).matchesDirectly(relativePath, file.isDirectory());
+    }
+    return false;
+  }
+
+  @Nullable
+  private static String getBlazePackageRelativePathToFile(@Nullable PsiFileSystemItem file) {
+    if (file == null) {
+      return null;
+    }
+    BlazePackage containingPackage = BlazePackage.getContainingPackage(file);
+    if (containingPackage == null) {
+      return null;
+    }
+    return containingPackage.getRelativePathToChild(file.getVirtualFile());
+  }
+
+  @Override
+  public boolean handlesElement(PsiElement element) {
+    return super.handlesElement(element);
+  }
+
+  @Nullable
+  @Override
+  public Collection<? extends PsiElement> getElementsToSearch(@NotNull PsiElement element,
+                                                              @Nullable Module module,
+                                                              @NotNull Collection<PsiElement> allElementsToDelete) {
+    return super.getElementsToSearch(element, module, allElementsToDelete);
+  }
+
+  @Nullable
+  @Override
+  public Collection<PsiElement> getAdditionalElementsToDelete(@NotNull PsiElement element,
+                                                              @NotNull Collection<PsiElement> allElementsToDelete,
+                                                              boolean askUser) {
+    return super.getAdditionalElementsToDelete(element, allElementsToDelete, askUser);
+  }
+
+  @Nullable
+  @Override
+  public Collection<String> findConflicts(@NotNull PsiElement element, @NotNull PsiElement[] allElementsToDelete) {
+    return super.findConflicts(element, allElementsToDelete);
+  }
+
+  @Nullable
+  @Override
+  public UsageInfo[] preprocessUsages(Project project, UsageInfo[] usages) {
+    return usages;
+  }
+
+  @Override
+  public void prepareForDeletion(PsiElement element) throws IncorrectOperationException {
+  }
+
+  @Override
+  public boolean isToSearchInComments(PsiElement element) {
+    return super.isToSearchInComments(element);
+  }
+
+  @Override
+  public void setToSearchInComments(PsiElement element, boolean enabled) {
+    super.setToSearchInComments(element, enabled);
+  }
+
+  @Override
+  public boolean isToSearchForTextOccurrences(PsiElement element) {
+    return super.isToSearchForTextOccurrences(element);
+  }
+
+  @Override
+  public void setToSearchForTextOccurrences(PsiElement element, boolean enabled) {
+    super.setToSearchForTextOccurrences(element, enabled);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java b/blaze-java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java
new file mode 100644
index 0000000..143ba48
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/libraries/AttachSourceJarAction.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.libraries;
+
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.projectstructure.LibraryEditor;
+import com.intellij.CommonBundle;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.roots.libraries.LibraryTable;
+import com.intellij.openapi.ui.Messages;
+import org.jetbrains.annotations.NotNull;
+
+public class AttachSourceJarAction extends BlazeAction {
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    assert project != null;
+    Library library = LibraryActionHelper.findLibraryForAction(e);
+    if (library != null) {
+      BlazeLibrary blazeLibrary = LibraryActionHelper.findLibraryFromIntellijLibrary(project, library);
+      if (blazeLibrary == null) {
+        Messages.showErrorDialog(project, "Could not find this library in the project.", CommonBundle.getErrorTitle());
+        return;
+      }
+
+      final LibraryArtifact libraryArtifact = blazeLibrary.getLibraryArtifact();
+      if (libraryArtifact == null) {
+        return;
+      }
+      if (libraryArtifact.sourceJar == null) {
+        return;
+      }
+      SourceJarManager sourceJarManager = SourceJarManager.getInstance(project);
+      boolean attachSourceJar = !sourceJarManager.hasSourceJarAttached(blazeLibrary.getKey());
+      sourceJarManager.setHasSourceJarAttached(blazeLibrary.getKey(), attachSourceJar);
+
+      ApplicationManager.getApplication().runWriteAction(() -> {
+        LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
+        LibraryTable.ModifiableModel libraryTableModel =
+          libraryTable.getModifiableModel();
+        LibraryEditor.updateLibrary(libraryTable, libraryTableModel, blazeLibrary, attachSourceJar);
+        libraryTableModel.commit();
+      });
+    }
+  }
+
+  @Override
+  protected void doUpdate(@NotNull AnActionEvent e) {
+    Presentation presentation = e.getPresentation();
+    String text = "Attach Source Jar";
+    boolean visible = false;
+    boolean enabled = false;
+    Project project = e.getProject();
+    if (project != null) {
+      Library library = LibraryActionHelper.findLibraryForAction(e);
+      if (library != null) {
+        visible = true;
+
+        BlazeLibrary blazeLibrary = LibraryActionHelper.findLibraryFromIntellijLibrary(e.getProject(), library);
+        if (blazeLibrary != null && blazeLibrary.getLibraryArtifact() != null && blazeLibrary.getLibraryArtifact().sourceJar != null) {
+          enabled = true;
+          if (SourceJarManager.getInstance(project).hasSourceJarAttached(blazeLibrary.getKey())) {
+            text = "Detach Source Jar";
+          }
+        }
+      }
+    }
+    presentation.setVisible(visible);
+    presentation.setEnabled(enabled);
+    presentation.setText(text);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java b/blaze-java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java
new file mode 100644
index 0000000..c9475a1
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/libraries/BlazeAttachSourceProvider.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.libraries;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import com.google.idea.blaze.java.sync.projectstructure.LibraryEditor;
+import com.intellij.codeInsight.AttachSourcesProvider;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.LibraryOrderEntry;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.roots.libraries.LibraryTable;
+import com.intellij.openapi.util.ActionCallback;
+import com.intellij.psi.PsiFile;
+import com.intellij.util.ui.UIUtil;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Sergey Evdokimov
+ */
+public class BlazeAttachSourceProvider implements AttachSourcesProvider {
+  private static final Logger LOG = Logger.getInstance(BlazeAttachSourceProvider.class);
+
+  @NotNull
+  @Override
+  public Collection<AttachSourcesAction> getActions(List<LibraryOrderEntry> orderEntries, final PsiFile psiFile) {
+    Project project = psiFile.getProject();
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return ImmutableList.of();
+    }
+
+    List<BlazeLibrary> librariesToAttachSourceTo = Lists.newArrayList();
+    for (LibraryOrderEntry orderEntry : orderEntries) {
+      Library library = orderEntry.getLibrary();
+      if (library == null) {
+        continue;
+      }
+      LibraryKey libraryKey = LibraryKey.fromIntelliJLibrary(library);
+      if (SourceJarManager.getInstance(project).hasSourceJarAttached(libraryKey)) {
+        continue;
+      }
+      BlazeLibrary blazeLibrary = LibraryActionHelper.findLibraryFromIntellijLibrary(project, library);
+      if (blazeLibrary == null) {
+        continue;
+      }
+      LibraryArtifact libraryArtifact = blazeLibrary.getLibraryArtifact();
+      if (libraryArtifact == null) {
+        continue;
+      }
+      ArtifactLocation artifactLocation = libraryArtifact.sourceJar;
+      if (artifactLocation == null) {
+        continue;
+      }
+      librariesToAttachSourceTo.add(blazeLibrary);
+    }
+
+    if (librariesToAttachSourceTo.isEmpty()) {
+      return ImmutableList.of();
+    }
+
+    /**
+     * Semi-hack: When sources are requested and we have them, we attach
+     * them automatically if the corresponding user setting is active.
+     */
+    if (BlazeUserSettings.getInstance().getAttachSourcesOnDemand()) {
+      UIUtil.invokeLaterIfNeeded(() -> {
+        attachSources(project, librariesToAttachSourceTo);
+      });
+      return ImmutableList.of();
+    }
+
+    return ImmutableList.of(new AttachSourcesAction() {
+      @Override
+      public String getName() {
+        return "Attach Blaze Source Jars";
+      }
+
+      @Override
+      public String getBusyText() {
+        return "Attaching source jars...";
+      }
+
+      @Override
+      public ActionCallback perform(List<LibraryOrderEntry> orderEntriesContainingFile) {
+        attachSources(project, librariesToAttachSourceTo);
+        return ActionCallback.DONE;
+      }
+    });
+  }
+
+  static void attachSources(Project project, Collection<BlazeLibrary> librariesToAttachSourceTo) {
+    ApplicationManager.getApplication().runWriteAction(() -> {
+      LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
+      LibraryTable.ModifiableModel libraryTableModel =
+        libraryTable.getModifiableModel();
+      for (BlazeLibrary blazeLibrary : librariesToAttachSourceTo) {
+        // Make sure we don't do it twice
+        if (SourceJarManager.getInstance(project).hasSourceJarAttached(blazeLibrary.getKey())) {
+          continue;
+        }
+        LibraryEditor.updateLibrary(libraryTable, libraryTableModel, blazeLibrary, true);
+        SourceJarManager.getInstance(project).setHasSourceJarAttached(blazeLibrary.getKey(), true);
+      }
+      libraryTableModel.commit();
+    });
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java b/blaze-java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java
new file mode 100644
index 0000000..330e811
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/libraries/ExcludeLibraryAction.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.libraries;
+
+import com.google.idea.blaze.base.actions.BlazeAction;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.projectview.ProjectViewEdit;
+import com.google.idea.blaze.base.projectview.section.Glob;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.sync.BlazeSyncManager;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.java.projectview.ExcludeLibrarySection;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.intellij.CommonBundle;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.Presentation;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.ui.Messages;
+import org.jetbrains.annotations.NotNull;
+
+public class ExcludeLibraryAction extends BlazeAction {
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    Project project = e.getProject();
+    assert project != null;
+    Library library = LibraryActionHelper.findLibraryForAction(e);
+    if (library != null) {
+      BlazeLibrary blazeLibrary = LibraryActionHelper.findLibraryFromIntellijLibrary(project, library);
+      if (blazeLibrary == null) {
+        Messages.showErrorDialog(project, "Could not find this library in the project.", CommonBundle.getErrorTitle());
+        return;
+      }
+
+      final LibraryArtifact libraryArtifact = blazeLibrary.getLibraryArtifact();
+      if (libraryArtifact == null) {
+        return;
+      }
+
+      final String path = libraryArtifact.jar.getRelativePath();
+
+      ProjectViewEdit edit = ProjectViewEdit.editLocalProjectView(project, builder -> {
+        builder.put(ListSection
+          .update(ExcludeLibrarySection.KEY, builder.get(ExcludeLibrarySection.KEY))
+          .add(new Glob(path))
+        );
+        return true;
+      });
+      edit.apply();
+
+      BlazeSyncManager.getInstance(project).requestProjectSync(new BlazeSyncParams.Builder(
+        "Sync",
+        BlazeSyncParams.SyncMode.INCREMENTAL
+      ).setDoBuild(false).build());
+    }
+  }
+
+  @Override
+  protected void doUpdate(@NotNull AnActionEvent e) {
+    Presentation presentation = e.getPresentation();
+    boolean enabled = LibraryActionHelper.findLibraryForAction(e) != null;
+    presentation.setVisible(enabled);
+    presentation.setEnabled(enabled);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java b/blaze-java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
new file mode 100644
index 0000000..ad52920
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/libraries/LibraryActionHelper.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.libraries;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import com.intellij.ide.projectView.impl.nodes.NamedLibraryElementNode;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.roots.libraries.LibraryTable;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.pom.Navigatable;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+public class LibraryActionHelper {
+
+  static BlazeLibrary findLibraryFromIntellijLibrary(Project project, Library library) {
+    LibraryKey libraryKey = LibraryKey.fromIntelliJLibrary(library);
+    BlazeProjectData projectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (projectData == null) {
+      return null;
+    }
+    BlazeJavaSyncData syncData = projectData.syncState.get(BlazeJavaSyncData.class);
+    if (syncData == null) {
+      Messages.showErrorDialog(project, "Project isn't synced. Please resync project.", "Error");
+      return null;
+    }
+
+    return syncData.importResult.libraries.get(libraryKey);
+  }
+
+  @Nullable
+  public static Library findLibraryForAction(@NotNull AnActionEvent e) {
+    Project project = e.getProject();
+    if (project != null) {
+      NamedLibraryElementNode node = findLibraryNode(e.getDataContext());
+      if (node != null) {
+        String libraryName = node.getName();
+        if (StringUtil.isNotEmpty(libraryName)) {
+          LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
+          return libraryTable.getLibraryByName(libraryName);
+        }
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  private static NamedLibraryElementNode findLibraryNode(@NotNull DataContext dataContext) {
+    Navigatable[] navigatables = CommonDataKeys.NAVIGATABLE_ARRAY.getData(dataContext);
+    if (navigatables != null && navigatables.length == 1) {
+      Navigatable navigatable = navigatables[0];
+      if (navigatable instanceof NamedLibraryElementNode) {
+        return (NamedLibraryElementNode)navigatable;
+      }
+    }
+    return null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/libraries/SourceJarManager.java b/blaze-java/src/com/google/idea/blaze/java/libraries/SourceJarManager.java
new file mode 100644
index 0000000..17bc762
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/libraries/SourceJarManager.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.libraries;
+
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import com.intellij.openapi.components.*;
+import com.intellij.openapi.project.Project;
+import org.jdom.Element;
+
+import java.util.Set;
+
+/**
+ * Keeps track of which libraries have source jars attached.
+ */
+@State(name = "BlazeSourceJarManager", storages = @Storage(StoragePathMacros.WORKSPACE_FILE))
+public class SourceJarManager implements PersistentStateComponent<Element> {
+  private Set<LibraryKey> librariesWithSourceJarsAttached = Sets.newHashSet();
+
+  public static SourceJarManager getInstance(Project project) {
+    return ServiceManager.getService(project, SourceJarManager.class);
+  }
+
+  public boolean hasSourceJarAttached(LibraryKey libraryKey) {
+    return librariesWithSourceJarsAttached.contains(libraryKey);
+  }
+
+  public void setHasSourceJarAttached(LibraryKey libraryKey, boolean hasSourceJar) {
+    if (hasSourceJar) {
+      librariesWithSourceJarsAttached.add(libraryKey);
+    } else {
+      librariesWithSourceJarsAttached.remove(libraryKey);
+    }
+  }
+
+  @Override
+  public Element getState() {
+    Element element = new Element("state");
+    for (LibraryKey libraryKey : librariesWithSourceJarsAttached) {
+      Element libElement = new Element("library");
+      libElement.setText(libraryKey.getIntelliJLibraryName());
+      element.addContent(libElement);
+    }
+    return element;
+  }
+
+  @Override
+  public void loadState(Element state) {
+    Set<LibraryKey> librariesWithSourceJars = Sets.newHashSet();
+    for (Element libElement : state.getChildren()) {
+      LibraryKey libraryKey = LibraryKey.fromIntelliJLibraryName(libElement.getText());
+      librariesWithSourceJars.add(libraryKey);
+    }
+    this.librariesWithSourceJarsAttached = librariesWithSourceJars;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/projectview/ExcludeLibrarySection.java b/blaze-java/src/com/google/idea/blaze/java/projectview/ExcludeLibrarySection.java
new file mode 100644
index 0000000..5323201
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/projectview/ExcludeLibrarySection.java
@@ -0,0 +1,26 @@
+/*
+ * 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.projectview;
+
+import com.google.idea.blaze.base.projectview.section.*;
+
+/**
+ * Section for excluding libraries.
+ */
+public class ExcludeLibrarySection {
+  public static final SectionKey<Glob, ListSection<Glob>> KEY = SectionKey.of("exclude_library");
+  public static final SectionParser PARSER = new GlobSectionParser(KEY);
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/projectview/ExcludedLibrarySection.java b/blaze-java/src/com/google/idea/blaze/java/projectview/ExcludedLibrarySection.java
new file mode 100644
index 0000000..5988d3b
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/projectview/ExcludedLibrarySection.java
@@ -0,0 +1,32 @@
+/*
+ * 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.projectview;
+
+import com.google.idea.blaze.base.projectview.section.*;
+
+/**
+ * Section for excluding libraries.
+ */
+@Deprecated
+public class ExcludedLibrarySection {
+  public static final SectionKey<Glob, ListSection<Glob>> KEY = SectionKey.of("excluded_libraries");
+  public static final SectionParser PARSER = new GlobSectionParser(KEY) {
+    @Override
+    public boolean isDeprecated() {
+      return true;
+    }
+  };
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/projectview/JavaLanguageLevelSection.java b/blaze-java/src/com/google/idea/blaze/java/projectview/JavaLanguageLevelSection.java
new file mode 100644
index 0000000..b94b60d
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/projectview/JavaLanguageLevelSection.java
@@ -0,0 +1,95 @@
+/*
+ * 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.projectview;
+
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.parser.ParseContext;
+import com.google.idea.blaze.base.projectview.parser.ProjectViewParser;
+import com.google.idea.blaze.base.projectview.section.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.intellij.pom.java.LanguageLevel;
+
+import javax.annotation.Nullable;
+
+public class JavaLanguageLevelSection {
+  public static final SectionKey<Integer, ScalarSection<Integer>> KEY =
+    SectionKey.of("java_language_level");
+  public static final SectionParser PARSER = new JavaLanguageLevelParser();
+
+  public static LanguageLevel getLanguageLevel(ProjectViewSet projectViewSet, LanguageLevel defaultValue) {
+    Integer level = projectViewSet.getSectionValue(KEY, null);
+    if (level == null) {
+      return defaultValue;
+    }
+    return getLanguageLevel(level, defaultValue);
+  }
+
+  @Nullable
+  private static LanguageLevel getLanguageLevel(Integer level, @Nullable LanguageLevel defaultValue) {
+    switch (level) {
+      case 3:
+        return LanguageLevel.JDK_1_3;
+      case 4:
+        return LanguageLevel.JDK_1_4;
+      case 5:
+        return LanguageLevel.JDK_1_5;
+      case 6:
+        return LanguageLevel.JDK_1_6;
+      case 7:
+        return LanguageLevel.JDK_1_7;
+      case 8:
+        return LanguageLevel.JDK_1_8;
+      case 9:
+        return LanguageLevel.JDK_1_9;
+      default:
+        return defaultValue;
+    }
+  }
+
+  private static class JavaLanguageLevelParser extends ScalarSectionParser<Integer> {
+    public JavaLanguageLevelParser() {
+      super(KEY, ':');
+    }
+
+    @Nullable
+    @Override
+    protected Integer parseItem(ProjectViewParser parser, ParseContext parseContext, String rest) {
+      try {
+        Integer value = Integer.parseInt(rest);
+        if (getLanguageLevel(value, null) != null) {
+          return value;
+        }
+        // Fall through to error handler
+      } catch (NumberFormatException e) {
+        // Fall through to error handler
+      }
+      parseContext.addError("Illegal java language level: " + rest);
+      return null;
+    }
+
+    @Override
+    protected void printItem(StringBuilder sb, Integer value) {
+      sb.append(value.toString());
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Other;
+    }
+  }
+}
\ No newline at end of file
diff --git a/blaze-java/src/com/google/idea/blaze/java/psi/JavaPsiFileProvider.java b/blaze-java/src/com/google/idea/blaze/java/psi/JavaPsiFileProvider.java
new file mode 100644
index 0000000..1844300
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/psi/JavaPsiFileProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.psi;
+
+import com.google.idea.blaze.base.lang.buildfile.search.PsiFileProvider;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+
+import javax.annotation.Nullable;
+
+/**
+ * Replaces top-level java classes with their corresponding PsiFile
+ */
+public class JavaPsiFileProvider implements PsiFileProvider {
+
+  @Nullable
+  @Override
+  public PsiFile asFileSearch(PsiElement elementToSearch) {
+    if (elementToSearch instanceof PsiClass) {
+      elementToSearch = elementToSearch.getParent();
+    }
+    return elementToSearch instanceof PsiFile ? (PsiFile) elementToSearch : null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandDebuggerRunner.java b/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandDebuggerRunner.java
new file mode 100644
index 0000000..cb68930
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandDebuggerRunner.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import com.intellij.debugger.impl.GenericDebuggerRunner;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.*;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.execution.ui.RunContentDescriptor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A runner that adapts the GenericDebuggerRunner to work with Blaze run configurations.
+ */
+public class BlazeCommandDebuggerRunner extends GenericDebuggerRunner {
+  @Override
+  @NotNull
+  public String getRunnerId() {
+    return "Blaze-Debug";
+  }
+
+  @Override
+  public boolean canRun(@NotNull final String executorId, @NotNull final RunProfile profile) {
+    return executorId.equals(DefaultDebugExecutor.EXECUTOR_ID)
+           && profile instanceof BlazeCommandRunConfiguration;
+  }
+
+  @Override
+  public void patch(JavaParameters javaParameters, RunnerSettings runnerSettings,
+                    RunProfile runProfile, final boolean beforeExecution) {
+    // We don't want to support Java run configuration patching.
+  }
+
+  @Override
+  @Nullable
+  public RunContentDescriptor createContentDescriptor(
+    @NotNull RunProfileState state, @NotNull ExecutionEnvironment environment)
+    throws ExecutionException {
+    if (!(state instanceof BlazeCommandRunProfileState)) {
+      return null;
+    }
+    BlazeCommandRunProfileState blazeState = (BlazeCommandRunProfileState)state;
+    RemoteConnection connection = blazeState.getRemoteConnection();
+    return attachVirtualMachine(state, environment, connection, true /* pollConnection */);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunConfiguration.java b/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunConfiguration.java
new file mode 100644
index 0000000..bf97eea
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunConfiguration.java
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.LocatableConfigurationBase;
+import com.intellij.execution.configurations.RuntimeConfigurationError;
+import com.intellij.execution.configurations.RuntimeConfigurationException;
+import com.intellij.execution.executors.DefaultDebugExecutor;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.options.SettingsEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import com.intellij.util.execution.ParametersListUtil;
+import org.jdom.Element;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.util.List;
+
+/**
+ * A run configuration which executes Blaze commands.
+ */
+public class BlazeCommandRunConfiguration extends LocatableConfigurationBase implements BlazeRunConfiguration {
+  private static final String TARGET_TAG = "blaze-target";
+  private static final String COMMAND_ATTR = "blaze-command";
+  private static final String USER_BLAZE_FLAG_TAG = "blaze-user-flag";
+  private static final String USER_EXE_FLAG_TAG = "blaze-user-exe-flag";
+
+  protected final String buildSystemName;
+
+  @Nullable private TargetExpression target;
+  @Nullable private BlazeCommandName command;
+  private ImmutableList<String> blazeFlags = ImmutableList.of();
+  private ImmutableList<String> exeFlags = ImmutableList.of();
+
+  public BlazeCommandRunConfiguration(Project project,
+                                      ConfigurationFactory factory,
+                                      String name) {
+    super(project, factory, name);
+    buildSystemName = Blaze.buildSystemName(project);
+  }
+
+  @Override
+  @Nullable
+  public TargetExpression getTarget() {
+    return target;
+  }
+
+  @Nullable
+  public BlazeCommandName getCommand() {
+    return command;
+  }
+
+  /**
+   * @return The list of blaze flags that the user specified manually.
+   */
+  protected List<String> getBlazeFlags() {
+    return blazeFlags;
+  }
+
+  /**
+   * @return The list of executable flags the user specified manually.
+   */
+  protected List<String> getExeFlags() {
+    return exeFlags;
+  }
+
+  /**
+   * @return The list of all flags to be used on the Blaze command line for blaze. Subclasses should override this method to add flags for
+   * higher-level settings (e.g. "run locally").
+   */
+  public List<String> getAllBlazeFlags() {
+    return getBlazeFlags();
+  }
+
+  /**
+   * @return The list of all flags to be used for the executable on the Blaze command line. Subclasses should override this method to add
+   * flags if desired.
+   */
+  public List<String> getAllExeFlags() {
+    return getExeFlags();
+  }
+
+  public void setTarget(@Nullable TargetExpression target) {
+    this.target = target;
+  }
+
+  public void setCommand(@Nullable BlazeCommandName command) {
+    this.command = command;
+  }
+
+  public final void setBlazeFlags(List<String> flags) {
+    this.blazeFlags = ImmutableList.copyOf(flags);
+  }
+
+  public final void setExeFlags(List<String> flags) {
+    this.exeFlags = ImmutableList.copyOf(flags);
+  }
+
+  @Override
+  public void checkConfiguration() throws RuntimeConfigurationException {
+    if (target == null) {
+      throw new RuntimeConfigurationError(String.format("You must specify a %s target expression.", buildSystemName));
+    }
+    if (command == null) {
+      throw new RuntimeConfigurationError(String.format("You must specify a command.", buildSystemName));
+    }
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    super.readExternal(element);
+    // Target is persisted as a tag to permit multiple targets in the future.
+    Element targetElement = element.getChild(TARGET_TAG);
+    if (targetElement != null && !Strings.isNullOrEmpty(targetElement.getTextTrim())) {
+      target = TargetExpression.fromString(targetElement.getTextTrim());
+    }
+    else {
+      target = null;
+    }
+    String commandString = element.getAttributeValue(COMMAND_ATTR);
+    command = Strings.isNullOrEmpty(commandString) ?
+              null : BlazeCommandName.fromString(commandString);
+    blazeFlags = loadUserFlags(element, USER_BLAZE_FLAG_TAG);
+    exeFlags = loadUserFlags(element, USER_EXE_FLAG_TAG);
+  }
+
+  private static ImmutableList<String> loadUserFlags(Element root, String tag) {
+    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
+    for (Element e : root.getChildren(tag)) {
+      String flag = e.getTextTrim();
+      if (flag != null && !flag.isEmpty()) {
+        flagsBuilder.add(flag);
+      }
+    }
+    return flagsBuilder.build();
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    super.writeExternal(element);
+    if (target != null) {
+      // Target is persisted as a tag to permit multiple targets in the future.
+      Element targetElement = new Element(TARGET_TAG);
+      targetElement.setText(target.toString());
+      element.addContent(targetElement);
+    }
+    if (command != null) {
+      element.setAttribute(COMMAND_ATTR, command.toString());
+    }
+    saveUserFlags(element, blazeFlags, USER_BLAZE_FLAG_TAG);
+    saveUserFlags(element, exeFlags, USER_EXE_FLAG_TAG);
+  }
+
+  private static void saveUserFlags(Element root, List<String> flags, String tag) {
+    for (String flag : flags) {
+      Element child = new Element(tag);
+      child.setText(flag);
+      root.addContent(child);
+    }
+  }
+
+  @Override
+  public BlazeCommandRunConfiguration clone() {
+    final BlazeCommandRunConfiguration configuration = (BlazeCommandRunConfiguration)super.clone();
+    configuration.target = target;
+    configuration.command = command;
+    configuration.blazeFlags = blazeFlags;
+    configuration.exeFlags = exeFlags;
+    return configuration;
+  }
+
+  @Override
+  @Nullable
+  public String suggestedName() {
+    TargetExpression target = getTarget();
+    if (!(target instanceof Label)) {
+      return null;
+    }
+    return String.format("%s %s: %s", buildSystemName, command.toString(), ((Label)target).ruleName().toString());
+  }
+
+
+  @Nullable
+  @Override
+  public BlazeCommandRunProfileState getState(Executor executor,
+                                              ExecutionEnvironment environment) throws ExecutionException {
+    return new BlazeCommandRunProfileState(environment, executor instanceof DefaultDebugExecutor);
+  }
+
+  @Override
+  public SettingsEditor<? extends BlazeCommandRunConfiguration> getConfigurationEditor() {
+    return new BlazeCommandRunConfigurationSettingsEditor(buildSystemName);
+  }
+
+  @VisibleForTesting
+  static class BlazeCommandRunConfigurationSettingsEditor extends SettingsEditor<BlazeCommandRunConfiguration> {
+    private final String buildSystemName;
+    private final JTextField targetField = new JTextField();
+    private final ComboBox commandCombo;
+    private final JTextArea blazeFlagsField = new JTextArea(5, 0);
+    private final JTextArea exeFlagsField = new JTextArea(5, 0);
+
+    public BlazeCommandRunConfigurationSettingsEditor(String buildSystemName) {
+      this.buildSystemName = buildSystemName;
+      commandCombo = new ComboBox(
+        new DefaultComboBoxModel(BlazeCommandName.knownCommands().toArray()));
+      // Allow the user to manually specify an unlisted command.
+      commandCombo.setEditable(true);
+    }
+
+    @VisibleForTesting
+    @Override
+    public void resetEditorFrom(BlazeCommandRunConfiguration s) {
+      targetField.setText(s.target == null ? null : s.target.toString());
+      commandCombo.setSelectedItem(s.command);
+      blazeFlagsField.setText(ParametersListUtil.join(s.blazeFlags));
+      exeFlagsField.setText(ParametersListUtil.join(s.exeFlags));
+    }
+
+    @VisibleForTesting
+    @Override
+    public void applyEditorTo(BlazeCommandRunConfiguration s) throws ConfigurationException {
+      String targetString = targetField.getText();
+      s.target = Strings.isNullOrEmpty(targetString) ?
+                 null : TargetExpression.fromString(targetString);
+      Object selectedCommand = commandCombo.getSelectedItem();
+      if (selectedCommand instanceof BlazeCommandName) {
+        s.command = (BlazeCommandName)selectedCommand;
+      }
+      else {
+        s.command = Strings.isNullOrEmpty((String)selectedCommand) ?
+                    null : BlazeCommandName.fromString(selectedCommand.toString());
+      }
+      s.blazeFlags = ImmutableList.copyOf(ParametersListUtil.parse(Strings.nullToEmpty(blazeFlagsField.getText())));
+      s.exeFlags = ImmutableList.copyOf(ParametersListUtil.parse(Strings.nullToEmpty(exeFlagsField.getText())));
+    }
+
+    @Override
+    protected JComponent createEditor() {
+      return UiUtil.createBox(
+        new JLabel("Target expression:"),
+        targetField,
+        new JLabel(buildSystemName + " command:"),
+        commandCombo,
+        new JLabel(buildSystemName +" flags:"),
+        blazeFlagsField,
+        new JLabel("Executable flags:"),
+        exeFlagsField
+      );
+    }
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunConfigurationType.java b/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunConfigurationType.java
new file mode 100644
index 0000000..c3705a3
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunConfigurationType.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.BeforeRunTask;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.ConfigurationType;
+import com.intellij.execution.configurations.ConfigurationTypeUtil;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import icons.BlazeIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+
+/**
+ * A type for run configurations that execute Blaze commands.
+ */
+public class BlazeCommandRunConfigurationType implements ConfigurationType {
+  @NotNull
+  private final BlazeCommandRunConfigurationFactory factory =
+    new BlazeCommandRunConfigurationFactory(this);
+
+  public static class BlazeCommandRunConfigurationFactory extends ConfigurationFactory {
+    protected BlazeCommandRunConfigurationFactory(@NotNull ConfigurationType type) {
+      super(type);
+    }
+
+    @Override
+    public boolean isApplicable(@NotNull Project project) {
+      return Blaze.isBlazeProject(project);
+    }
+
+    @Override
+    public BlazeCommandRunConfiguration createTemplateConfiguration(Project project) {
+      return new BlazeCommandRunConfiguration(project, this, "Unnamed");
+    }
+
+    @Override
+    public void configureBeforeRunTaskDefaults(
+      Key<? extends BeforeRunTask> providerID, BeforeRunTask task) {
+      // We don't need or want any before run tasks by default.
+      task.setEnabled(false);
+    }
+
+    @Override
+    public boolean isConfigurationSingletonByDefault() {
+      return true;
+    }
+  }
+
+  @NotNull
+  public static BlazeCommandRunConfigurationType getInstance() {
+    return ConfigurationTypeUtil.findConfigurationType(BlazeCommandRunConfigurationType.class);
+  }
+
+  @NotNull
+  @Override
+  public String getDisplayName() {
+    return Blaze.defaultBuildSystemName() + " Command";
+  }
+
+  @NotNull
+  @Override
+  public String getConfigurationTypeDescription() {
+    return String.format("Configuration for launching arbitrary %s commands.", Blaze.guessBuildSystemName());
+  }
+
+  @NotNull
+  @Override
+  public Icon getIcon() {
+    return BlazeIcons.Blaze;
+  }
+
+  @NotNull
+  @Override
+  public String getId() {
+    return "BlazeCommandRunConfigurationType";
+  }
+
+  @NotNull
+  @Override
+  public BlazeCommandRunConfigurationFactory[] getConfigurationFactories() {
+    return new BlazeCommandRunConfigurationFactory[]{factory};
+  }
+
+  @NotNull
+  public BlazeCommandRunConfigurationFactory getFactory() {
+    return factory;
+  }
+
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunProfileState.java b/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunProfileState.java
new file mode 100644
index 0000000..93bef0b
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/BlazeCommandRunProfileState.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.run.processhandler.LineProcessingProcessAdapter;
+import com.google.idea.blaze.base.run.processhandler.ScopedBlazeProcessHandler;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.CommandLineState;
+import com.intellij.execution.configurations.RemoteConnection;
+import com.intellij.execution.configurations.RemoteState;
+import com.intellij.execution.configurations.RunProfile;
+import com.intellij.execution.process.ProcessHandler;
+import com.intellij.execution.process.ProcessListener;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.project.Project;
+
+/**
+ * A Blaze run configuration set up with a an executor, program runner, and other settings, ready to
+ * be executed. This class creates a command line for Blaze and exposes debug connection information
+ * when using a debug executor.
+ */
+final class BlazeCommandRunProfileState extends CommandLineState implements RemoteState {
+  // Blaze seems to always use this port for --java_debug.
+  // TODO(joshgiles): Look at manually identifying and setting port.
+  private static final int DEBUG_PORT = 5005;
+  private static final String DEBUG_HOST_NAME = "localhost";
+
+  private final BlazeCommandRunConfiguration configuration;
+  private final boolean debug;
+
+  public BlazeCommandRunProfileState(ExecutionEnvironment environment, boolean debug) {
+    super(environment);
+    RunProfile runProfile = environment.getRunProfile();
+    assert runProfile instanceof BlazeCommandRunConfiguration;
+    configuration = (BlazeCommandRunConfiguration)runProfile;
+    this.debug = debug;
+  }
+
+  @Override
+  protected ProcessHandler startProcess() throws ExecutionException {
+    Project project = configuration.getProject();
+    BlazeImportSettings importSettings =
+      BlazeImportSettingsManager.getInstance(project).getImportSettings();
+    assert importSettings != null;
+
+    ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+    assert projectViewSet != null;
+
+    BlazeCommand blazeCommand = getBlazeCommand(project, configuration, projectViewSet, debug);
+    WorkspaceRoot workspaceRoot = WorkspaceRoot.fromImportSettings(importSettings);
+    return new ScopedBlazeProcessHandler(
+      blazeCommand,
+      workspaceRoot,
+      new ScopedBlazeProcessHandler.ScopedProcessHandlerDelegate() {
+        @Override
+        public void onBlazeContextStart(BlazeContext context) {
+          context
+            .push(new LoggedTimingScope(project, Action.BLAZE_COMMAND_USAGE))
+            .push(new IssuesScope(project))
+          ;
+        }
+
+        @Override
+        public ImmutableList<ProcessListener> createProcessListeners(BlazeContext context) {
+          LineProcessingOutputStream outputStream = LineProcessingOutputStream.of(
+            new IssueOutputLineProcessor(project, context, workspaceRoot)
+          );
+          return ImmutableList.of(new LineProcessingProcessAdapter(outputStream));
+        }
+      }
+    );
+  }
+
+  @Override
+  public RemoteConnection getRemoteConnection() {
+    if (!debug) {
+      return null;
+    }
+    return new RemoteConnection(true /* useSockets */,
+                                DEBUG_HOST_NAME,
+                                Integer.toString(DEBUG_PORT),
+                                false /* serverMode */
+    );
+  }
+
+  @VisibleForTesting
+  static BlazeCommand getBlazeCommand(
+    Project project,
+    BlazeCommandRunConfiguration configuration,
+    ProjectViewSet projectViewSet,
+    boolean debug) {
+
+    BlazeCommandName blazeCommand = configuration.getCommand();
+    assert blazeCommand != null;
+    BlazeCommand.Builder command = BlazeCommand.builder(Blaze.getBuildSystem(project), blazeCommand)
+      .addTargets(configuration.getTarget())
+      .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+      .addBlazeFlags(configuration.getAllBlazeFlags())
+      ;
+
+    if (debug) {
+      boolean isJavaBinary = false;
+      TargetExpression targetExpression = configuration.getTarget();
+      if (targetExpression instanceof Label) {
+        RuleIdeInfo rule = RuleFinder.getInstance().ruleForTarget(configuration.getProject(), (Label)targetExpression);
+        if (rule != null && (rule.kind == Kind.JAVA_BINARY)) {
+          isJavaBinary = true;
+        }
+      }
+
+      if (isJavaBinary) {
+        command.addExeFlags(BlazeFlags.JAVA_BINARY_DEBUG);
+      } else {
+        command.addBlazeFlags(BlazeFlags.JAVA_TEST_DEBUG);
+      }
+
+    }
+
+    command.addExeFlags(configuration.getAllExeFlags());
+    return command.build();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/RunUtil.java b/blaze-java/src/com/google/idea/blaze/java/run/RunUtil.java
new file mode 100644
index 0000000..963245e
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/RunUtil.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import com.google.common.collect.Iterables;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.run.TestRuleFinder;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiFile;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+
+/**
+ * Utility methods for finding rules and Android facets.
+ */
+public final class RunUtil {
+
+  private RunUtil() {
+  }
+
+  /**
+   * @return The Blaze test rule containing the target test class. In the case of multiple
+   * containing rules, the first rule sorted alphabetically by label.
+   */
+  @Nullable
+  public static RuleIdeInfo ruleForTestClass(@NotNull Project project,
+                                             @NotNull PsiClass testClass,
+                                             @Nullable TestIdeInfo.TestSize testSize) {
+    File testFile = getFileForClass(testClass);
+    if (testFile == null) {
+      return null;
+    }
+    TestRuleFinder testRuleFinder = TestRuleFinder.getInstance(project);
+    // We don't expose multiple rule choices, just pick first
+    Label testLabel = Iterables.getFirst(testRuleFinder.testTargetsForSourceFile(testFile, testSize), null);
+    if (testLabel == null) {
+      return null;
+    }
+    return RuleFinder.getInstance().ruleForTarget(project, testLabel);
+  }
+
+  /**
+   * Returns an instance of {@link java.io.File} related to the containing file of the given class.
+   * It returns {@code null} if the given class is not contained in a file and only exists in
+   * memory.
+   */
+  @Nullable
+  private static File getFileForClass(@NotNull PsiClass aClass) {
+    PsiFile containingFile = aClass.getContainingFile();
+    if (containingFile == null) {
+      return null;
+    }
+
+    VirtualFile virtualFile = containingFile.getVirtualFile();
+    if (virtualFile == null) {
+      return null;
+    }
+
+    return new File(virtualFile.getPath());
+  }
+
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/producers/AllInPackageBlazeConfigurationProducer.java b/blaze-java/src/com/google/idea/blaze/java/run/producers/AllInPackageBlazeConfigurationProducer.java
new file mode 100644
index 0000000..0dccb0f
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/producers/AllInPackageBlazeConfigurationProducer.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.java.run.producers;
+
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.java.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.java.run.BlazeCommandRunConfigurationType;
+import com.intellij.execution.ExecutionBundle;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.openapi.util.Ref;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.PsiDirectory;
+import com.intellij.psi.PsiElement;
+
+import javax.annotation.Nullable;
+
+/**
+ * Runs tests in all packages below selected directory
+ */
+public class AllInPackageBlazeConfigurationProducer extends BlazeTestRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  public AllInPackageBlazeConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+    BlazeCommandRunConfiguration configuration,
+    ConfigurationContext context,
+    Ref<PsiElement> sourceElement) {
+
+    PsiDirectory dir = getTestDirectory(context);
+    if (dir == null) {
+      return false;
+    }
+    WorkspaceRoot root = WorkspaceRoot.fromProject(context.getModule().getProject());
+    WorkspacePath packagePath = getWorkspaceRelativeDirectoryPath(root, dir);
+    if (packagePath == null) {
+      return false;
+    }
+    sourceElement.set(dir);
+
+    configuration.setCommand(BlazeCommandName.TEST);
+    configuration.setTarget(TargetExpression.allFromPackageRecursive(packagePath));
+    configuration.setName(
+      String.format("%s %s",
+                    Blaze.buildSystemName(context.getProject()),
+                    ExecutionBundle.message("test.in.scope.presentable.text", packagePath)));
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+    BlazeCommandRunConfiguration configuration,
+    ConfigurationContext context) {
+
+    PsiDirectory dir = getTestDirectory(context);
+    if (dir == null) {
+      return false;
+    }
+    WorkspaceRoot root = WorkspaceRoot.fromProject(context.getModule().getProject());
+    WorkspacePath packagePath = getWorkspaceRelativeDirectoryPath(root, dir);
+    if (packagePath == null) {
+      return false;
+    }
+    return configuration.getCommand() == BlazeCommandName.TEST &&
+           configuration.getTarget() == TargetExpression.allFromPackageRecursive(packagePath);
+  }
+
+  @Nullable
+  private static PsiDirectory getTestDirectory(ConfigurationContext context) {
+    WorkspaceRoot root = WorkspaceRoot.fromProject(context.getModule().getProject());
+    PsiElement location = context.getPsiLocation();
+    if (location instanceof PsiDirectory) {
+      PsiDirectory dir = (PsiDirectory) location;
+      if (isInWorkspace(root, dir)) {
+        return dir;
+      }
+    }
+    return null;
+  }
+
+  @Nullable
+  private static WorkspacePath getWorkspaceRelativeDirectoryPath(
+    WorkspaceRoot root,
+    PsiDirectory dir) {
+    VirtualFile file = dir.getVirtualFile();
+    if (isInWorkspace(root, dir)) {
+      return root.workspacePathFor(file);
+    }
+    return null;
+  }
+
+  private static boolean isInWorkspace(WorkspaceRoot root,
+                                       PsiDirectory dir) {
+    return root.isInWorkspace(dir.getVirtualFile());
+  }
+
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java b/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
new file mode 100644
index 0000000..7c1c39a
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestClassConfigurationProducer.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.google.idea.blaze.java.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.java.run.BlazeCommandRunConfigurationType;
+import com.intellij.execution.JavaExecutionUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Producer for run configurations related to Java test classes in Blaze.
+ */
+public class BlazeJavaTestClassConfigurationProducer extends BlazeTestRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  public BlazeJavaTestClassConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+    @NotNull BlazeCommandRunConfiguration configuration,
+    @NotNull ConfigurationContext context,
+    @NotNull Ref<PsiElement> sourceElement) {
+
+    final Location contextLocation = context.getLocation();
+    assert contextLocation != null;
+    final Location location = JavaExecutionUtil.stepIntoSingleClass(contextLocation);
+    if (location == null) {
+      return false;
+    }
+
+    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
+      return false;
+    }
+
+    PsiClass testClass = JUnitUtil.getTestClass(location);
+    if (testClass == null) {
+      return false;
+    }
+    sourceElement.set(testClass);
+
+    TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(testClass);
+    RuleIdeInfo rule = RunUtil.ruleForTestClass(context.getProject(), testClass, testSize);
+    if (rule == null) {
+      return false;
+    }
+
+    configuration.setCommand(BlazeCommandName.TEST);
+    configuration.setTarget(rule.label);
+
+    ImmutableList.Builder<String> flags = ImmutableList.builder();
+
+    String qualifiedName = testClass.getQualifiedName();
+    if (qualifiedName != null) {
+      flags.add(BlazeFlags.testFilterFlagForClass(qualifiedName));
+    }
+
+    flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
+
+    configuration.setBlazeFlags(flags.build());
+    configuration.setName(
+      String.format("%s test: %s (%s)",
+                    Blaze.buildSystemName(configuration.getProject()),
+                    testClass.getName(),
+                    rule.label.toString()));
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+    @NotNull BlazeCommandRunConfiguration configuration,
+    @NotNull ConfigurationContext context) {
+
+    final Location contextLocation = context.getLocation();
+    assert contextLocation != null;
+    final Location location = JavaExecutionUtil.stepIntoSingleClass(contextLocation);
+    if (location == null) {
+      return false;
+    }
+
+    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
+      return false;
+    }
+
+    Location<PsiMethod> methodLocation = ProducerUtils.getMethodLocation(contextLocation);
+    if (methodLocation != null) {
+      return false;
+    }
+
+    PsiClass testClass = JUnitUtil.getTestClass(location);
+    if (testClass == null) {
+      return false;
+    }
+
+    return checkIfAttributesAreTheSame(configuration, testClass);
+  }
+
+  private boolean checkIfAttributesAreTheSame(
+    @NotNull BlazeCommandRunConfiguration configuration,
+    @NotNull PsiClass testClass) {
+
+    List<String> flags = configuration.getAllBlazeFlags();
+
+    return flags.contains(BlazeFlags.testFilterFlagForClass(testClass.getQualifiedName()));
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java b/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
new file mode 100644
index 0000000..da906ca
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeJavaTestMethodConfigurationProducer.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.java.run.BlazeCommandRunConfiguration;
+import com.google.idea.blaze.java.run.BlazeCommandRunConfigurationType;
+import com.google.idea.blaze.java.run.RunUtil;
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiMethod;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.List;
+
+/**
+ * Producer for run configurations related to Java test methods in Blaze.
+ */
+public class BlazeJavaTestMethodConfigurationProducer extends BlazeTestRunConfigurationProducer<BlazeCommandRunConfiguration> {
+
+  public BlazeJavaTestMethodConfigurationProducer() {
+    super(BlazeCommandRunConfigurationType.getInstance());
+  }
+
+  @Override
+  protected boolean doSetupConfigFromContext(
+    @NotNull BlazeCommandRunConfiguration configuration,
+    @NotNull ConfigurationContext context,
+    @NotNull Ref<PsiElement> sourceElement) {
+
+    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
+      return false;
+    }
+
+    final Location contextLocation = context.getLocation();
+    assert contextLocation != null;
+    Location<PsiMethod> methodLocation = ProducerUtils.getMethodLocation(contextLocation);
+    if (methodLocation == null) {
+      return false;
+    }
+
+    final PsiMethod psiMethod = methodLocation.getPsiElement();
+    sourceElement.set(psiMethod);
+
+    final PsiClass containingClass = psiMethod.getContainingClass();
+    if (containingClass == null) {
+      return false;
+    }
+
+    TestIdeInfo.TestSize testSize = TestSizeAnnotationMap.getTestSize(psiMethod);
+    RuleIdeInfo rule = RunUtil.ruleForTestClass(context.getProject(), containingClass, testSize);
+    if (rule == null) {
+      return false;
+    }
+
+    configuration.setCommand(BlazeCommandName.TEST);
+    configuration.setTarget(rule.label);
+
+    ImmutableList.Builder<String> flags = ImmutableList.builder();
+
+    String qualifiedName = containingClass.getQualifiedName();
+    if (qualifiedName != null) {
+      flags.add(BlazeFlags.testFilterFlagForClassAndMethod(qualifiedName, psiMethod.getName()));
+    }
+
+    flags.add(BlazeFlags.TEST_OUTPUT_STREAMED);
+
+    configuration.setBlazeFlags(flags.build());
+    configuration.setName(
+      String.format("%s test: %s.%s (%s)",
+                    Blaze.buildSystemName(configuration.getProject()),
+                    containingClass.getName(),
+                    psiMethod.getName(),
+                    rule.label.toString()));
+    return true;
+  }
+
+  @Override
+  protected boolean doIsConfigFromContext(
+    @NotNull BlazeCommandRunConfiguration configuration,
+    @NotNull ConfigurationContext context) {
+
+    if (JUnitConfigurationUtil.isMultipleElementsSelected(context)) {
+      return false;
+    }
+
+    final Location contextLocation = context.getLocation();
+    assert contextLocation != null;
+
+    Location<PsiMethod> methodLocation = ProducerUtils.getMethodLocation(contextLocation);
+    if (methodLocation == null) {
+      return false;
+    }
+
+    final PsiMethod psiMethod = methodLocation.getPsiElement();
+    final PsiClass containingClass = psiMethod.getContainingClass();
+    if (containingClass == null) {
+      return false;
+    }
+
+    return checkIfAttributesAreTheSame(configuration, psiMethod);
+  }
+
+  private static boolean checkIfAttributesAreTheSame(
+    BlazeCommandRunConfiguration configuration, PsiMethod testMethod) {
+
+    List<String> flags = configuration.getAllBlazeFlags();
+
+    return flags.contains(BlazeFlags.testFilterFlagForClassAndMethod(testMethod.getContainingClass().getQualifiedName(), testMethod.getName()));
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeTestRunConfigurationProducer.java b/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeTestRunConfigurationProducer.java
new file mode 100644
index 0000000..fc0e439
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/producers/BlazeTestRunConfigurationProducer.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.actions.ConfigurationFromContext;
+import com.intellij.execution.actions.RunConfigurationProducer;
+import com.intellij.execution.configurations.ConfigurationType;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.util.NullUtils;
+import com.intellij.openapi.util.Ref;
+import com.intellij.psi.PsiElement;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Base class for Blaze test run configuration producers.
+ */
+public abstract class BlazeTestRunConfigurationProducer<T extends RunConfiguration> extends RunConfigurationProducer<T> {
+
+  protected BlazeTestRunConfigurationProducer(ConfigurationType configurationType) {
+    super(configurationType);
+  }
+
+  @Override
+  public boolean isPreferredConfiguration(ConfigurationFromContext self, ConfigurationFromContext other) {
+    return Blaze.isBlazeProject(self.getConfiguration().getProject());
+  }
+
+  @Override
+  public boolean shouldReplace(ConfigurationFromContext self, ConfigurationFromContext other) {
+    return Blaze.isBlazeProject(self.getConfiguration().getProject()) &&
+           !other.isProducedBy(BlazeTestRunConfigurationProducer.class);
+  }
+
+  @Override
+  protected final boolean setupConfigurationFromContext(T configuration, ConfigurationContext context, Ref<PsiElement> sourceElement) {
+    if (NullUtils.hasNull(configuration, context, sourceElement)) {
+      return false;
+    }
+    if (!validContext(context)) {
+      return false;
+    }
+    return doSetupConfigFromContext(configuration, context, sourceElement);
+  }
+
+  protected abstract boolean doSetupConfigFromContext(
+    @NotNull T configuration,
+    @NotNull ConfigurationContext context,
+    @NotNull Ref<PsiElement> sourceElement);
+
+  @Override
+  public final boolean isConfigurationFromContext(T configuration, ConfigurationContext context) {
+    if (NullUtils.hasNull(configuration, context)) {
+      return false;
+    }
+    if (!validContext(context)) {
+      return false;
+    }
+    return doIsConfigFromContext(configuration, context);
+  }
+
+  protected abstract boolean doIsConfigFromContext(
+    @NotNull T configuration,
+    @NotNull ConfigurationContext context);
+
+  private static boolean validContext(@NotNull ConfigurationContext context) {
+    Module module = context.getModule();
+    if (module == null) {
+      return false;
+    }
+    if (!isBlazeContext(context)) {
+      return false;
+    }
+    return true;
+  }
+
+  private static boolean isBlazeContext(@NotNull ConfigurationContext context) {
+    return Blaze.isBlazeProject(context.getProject());
+  }
+
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/producers/JUnitConfigurationUtil.java b/blaze-java/src/com/google/idea/blaze/java/run/producers/JUnitConfigurationUtil.java
new file mode 100644
index 0000000..fa757c4
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/producers/JUnitConfigurationUtil.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.intellij.execution.Location;
+import com.intellij.execution.actions.ConfigurationContext;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.execution.junit2.PsiMemberParameterizedLocation;
+import com.intellij.execution.junit2.info.MethodLocation;
+import com.intellij.execution.testframework.TestsUIUtil;
+import com.intellij.openapi.actionSystem.CommonDataKeys;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.actionSystem.LangDataKeys;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.psi.*;
+import com.intellij.psi.search.PsiElementProcessor;
+import com.intellij.psi.util.ClassUtil;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+/**
+ * Cloned from PatternConfigurationProducer, stripped down to only contain isMultipleElementsSelected.
+ */
+public class JUnitConfigurationUtil {
+  protected static boolean isTestClass(PsiClass psiClass) {
+    return JUnitUtil.isTestClass(psiClass);
+  }
+
+  protected static boolean isTestMethod(boolean checkAbstract, PsiElement psiElement) {
+    return JUnitUtil.getTestMethod(psiElement, checkAbstract) != null;
+  }
+
+  public static boolean isMultipleElementsSelected(ConfigurationContext context) {
+    final DataContext dataContext = context.getDataContext();
+    if (TestsUIUtil.isMultipleSelectionImpossible(dataContext)) return false;
+    final LinkedHashSet<String> classes = new LinkedHashSet<String>();
+    final PsiElementProcessor.CollectElementsWithLimit<PsiElement> processor = new PsiElementProcessor.CollectElementsWithLimit<PsiElement>(2);
+    final PsiElement[] locationElements = collectLocationElements(classes, dataContext);
+    if (locationElements != null) {
+      collectTestMembers(locationElements, false, false, processor);
+    }
+    else {
+      collectContextElements(dataContext, false, false, classes, processor);
+    }
+    return processor.getCollection().size() > 1;
+  }
+
+  public static void collectTestMembers(PsiElement[] psiElements,
+                                 boolean checkAbstract,
+                                 boolean checkIsTest,
+                                 PsiElementProcessor.CollectElements<PsiElement> collectingProcessor) {
+    for (PsiElement psiElement : psiElements) {
+      if (psiElement instanceof PsiClassOwner) {
+        final PsiClass[] classes = ((PsiClassOwner)psiElement).getClasses();
+        for (PsiClass aClass : classes) {
+          if ((!checkIsTest && aClass.hasModifierProperty(PsiModifier.PUBLIC) || checkIsTest && isTestClass(aClass)) && 
+              !collectingProcessor.execute(aClass)) {
+            return;
+          }
+        }
+      } else if (psiElement instanceof PsiClass) {
+        if ((!checkIsTest && ((PsiClass)psiElement).hasModifierProperty(PsiModifier.PUBLIC) || checkIsTest && isTestClass((PsiClass)psiElement)) && 
+            !collectingProcessor.execute(psiElement)) {
+          return;
+        }
+      } else if (psiElement instanceof PsiMethod) {
+        if (checkIsTest && isTestMethod(checkAbstract, psiElement) && !collectingProcessor.execute(psiElement)) {
+          return;
+        }
+        if (!checkIsTest) {
+          final PsiClass containingClass = ((PsiMethod)psiElement).getContainingClass();
+          if (containingClass != null && containingClass.hasModifierProperty(PsiModifier.PUBLIC) && !collectingProcessor.execute(psiElement)) {
+            return;
+          }
+        }
+      } else if (psiElement instanceof PsiDirectory) {
+        final PsiPackage aPackage = JavaDirectoryService.getInstance().getPackage((PsiDirectory)psiElement);
+        if (aPackage != null && !collectingProcessor.execute(aPackage)) {
+          return;
+        }
+      }
+    }
+  }
+
+  private static boolean collectContextElements(DataContext dataContext,
+                                         boolean checkAbstract,
+                                         boolean checkIsTest, 
+                                         LinkedHashSet<String> classes,
+                                         PsiElementProcessor.CollectElements<PsiElement> processor) {
+    PsiElement[] elements = LangDataKeys.PSI_ELEMENT_ARRAY.getData(dataContext);
+    if (elements != null) {
+      collectTestMembers(elements, checkAbstract, checkIsTest, processor);
+      for (PsiElement psiClass : processor.getCollection()) {
+        classes.add(getQName(psiClass));
+      }
+      return true;
+    }
+    else {
+      final VirtualFile[] files = CommonDataKeys.VIRTUAL_FILE_ARRAY.getData(dataContext);
+      if (files != null) {
+        Project project = CommonDataKeys.PROJECT.getData(dataContext);
+        if (project != null) {
+          final PsiManager psiManager = PsiManager.getInstance(project);
+          for (VirtualFile file : files) {
+            final PsiFile psiFile = psiManager.findFile(file);
+            if (psiFile instanceof PsiClassOwner) {
+              collectTestMembers(((PsiClassOwner)psiFile).getClasses(), checkAbstract, checkIsTest, processor);
+              for (PsiElement psiMember : processor.getCollection()) {
+                classes.add(((PsiClass)psiMember).getQualifiedName());
+              }
+            }
+          }
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private static PsiElement[] collectLocationElements(LinkedHashSet<String> classes, DataContext dataContext) {
+    final Location<?>[] locations = Location.DATA_KEYS.getData(dataContext);
+    if (locations != null) {
+      List<PsiElement> elements = new ArrayList<PsiElement>();
+      for (Location<?> location : locations) {
+        final PsiElement psiElement = location.getPsiElement();
+        classes.add(getQName(psiElement, location));
+        elements.add(psiElement);
+      }
+      return elements.toArray(new PsiElement[elements.size()]);
+    }
+    return null;
+  }
+
+  public static String getQName(PsiElement psiMember) {
+    return getQName(psiMember, null);
+  }
+
+  public static String getQName(PsiElement psiMember, Location location) {
+    if (psiMember instanceof PsiClass) {
+      return ClassUtil.getJVMClassName((PsiClass)psiMember);
+    }
+    else if (psiMember instanceof PsiMember) {
+      final PsiClass containingClass = location instanceof MethodLocation
+                                       ? ((MethodLocation)location).getContainingClass()
+                                       : location instanceof PsiMemberParameterizedLocation ? ((PsiMemberParameterizedLocation)location).getContainingClass() 
+                                                                                            : ((PsiMember)psiMember).getContainingClass();
+      assert containingClass != null;
+      return ClassUtil.getJVMClassName(containingClass) + "," + ((PsiMember)psiMember).getName();
+    } else if (psiMember instanceof PsiPackage) {
+      return ((PsiPackage)psiMember).getQualifiedName();
+    }
+    assert false;
+    return null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java b/blaze-java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java
new file mode 100644
index 0000000..bbcce9b
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/producers/ProducerUtils.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.intellij.execution.Location;
+import com.intellij.execution.junit.JUnitUtil;
+import com.intellij.execution.junit2.PsiMemberParameterizedLocation;
+import com.intellij.execution.junit2.info.MethodLocation;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiMethod;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Iterator;
+
+/**
+ * Copy of {@link org.jetbrains.plugins.gradle.execution.test.runner.TestRunnerUtils}.
+ * <p/>
+ * <p>Do not modify.
+ */
+public class ProducerUtils {
+  @Nullable
+  public static Location<PsiMethod> getMethodLocation(@NotNull Location contextLocation) {
+    Location<PsiMethod> methodLocation = getTestMethod(contextLocation);
+    if (methodLocation == null) {
+      return null;
+    }
+
+    if (contextLocation instanceof PsiMemberParameterizedLocation) {
+      PsiClass containingClass =
+        ((PsiMemberParameterizedLocation)contextLocation).getContainingClass();
+      if (containingClass != null) {
+        methodLocation = MethodLocation
+          .elementInClass(methodLocation.getPsiElement(), containingClass);
+      }
+    }
+    return methodLocation;
+  }
+
+  @Nullable
+  public static Location<PsiMethod> getTestMethod(final Location<?> location) {
+    for (Iterator<Location<PsiMethod>> iterator = location.getAncestors(PsiMethod.class, false);
+         iterator.hasNext(); ) {
+      final Location<PsiMethod> methodLocation = iterator.next();
+      if (JUnitUtil.isTestMethod(methodLocation, false)) {
+        return methodLocation;
+      }
+    }
+    return null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java b/blaze-java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java
new file mode 100644
index 0000000..0322bc0
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/run/producers/TestSizeAnnotationMap.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run.producers;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.TestIdeInfo;
+import com.intellij.psi.PsiAnnotation;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiModifierList;
+
+import javax.annotation.Nullable;
+
+/**
+ * Maps method and class annotations to our test size enumeration.
+ */
+public class TestSizeAnnotationMap {
+  private static ImmutableMap<String, TestIdeInfo.TestSize> ANNOTATION_TO_TEST_SIZE = ImmutableMap.<String, TestIdeInfo.TestSize>builder()
+    .put("com.google.testing.testsize.SmallTest", TestIdeInfo.TestSize.SMALL)
+    .put("com.google.testing.testsize.MediumTest", TestIdeInfo.TestSize.MEDIUM)
+    .put("com.google.testing.testsize.LargeTest", TestIdeInfo.TestSize.LARGE)
+    .put("com.google.testing.testsize.EnormousTest", TestIdeInfo.TestSize.ENORMOUS)
+    .build();
+
+  @Nullable
+  public static TestIdeInfo.TestSize getTestSize(PsiMethod psiMethod) {
+    PsiAnnotation[] annotations = psiMethod.getModifierList().getAnnotations();
+    TestIdeInfo.TestSize testSize = getTestSize(annotations);
+    if (testSize != null) {
+      return testSize;
+    }
+    return getTestSize(psiMethod.getContainingClass());
+  }
+
+  @Nullable
+  public static TestIdeInfo.TestSize getTestSize(PsiClass psiClass) {
+    PsiModifierList psiModifierList = psiClass.getModifierList();
+    if (psiModifierList == null) {
+      return null;
+    }
+    PsiAnnotation[] annotations = psiModifierList.getAnnotations();
+    TestIdeInfo.TestSize testSize = getTestSize(annotations);
+    if (testSize == null) {
+      return null;
+    }
+    return testSize;
+  }
+
+  @Nullable
+  private static TestIdeInfo.TestSize getTestSize(PsiAnnotation[] annotations) {
+    for (PsiAnnotation annotation : annotations) {
+      String qualifiedName = annotation.getQualifiedName();
+      TestIdeInfo.TestSize testSize = ANNOTATION_TO_TEST_SIZE.get(qualifiedName);
+      if (testSize != null) {
+        return testSize;
+      }
+    }
+    return null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java b/blaze-java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java
new file mode 100644
index 0000000..ba7219c
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncAugmenter.java
@@ -0,0 +1,46 @@
+/*
+ * 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;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.section.Glob;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.intellij.openapi.extensions.ExtensionPointName;
+
+import java.util.Collection;
+
+/**
+ * Augments the java importer
+ */
+public interface BlazeJavaSyncAugmenter {
+  ExtensionPointName<BlazeJavaSyncAugmenter> EP_NAME = ExtensionPointName.create("com.google.idea.blaze.java.JavaSyncAugmenter");
+
+  /**
+   * Adds any library filters. Useful if some libraries are supplied by this plugin in some other way, eg. via an SDK.
+   */
+  void addLibraryFilter(Glob.GlobSet excludedLibraries);
+
+  /**
+   * Called during the project structure phase to get additional libraries.
+   */
+  Collection<BlazeLibrary> getAdditionalLibraries(BlazeProjectData blazeProjectData);
+
+  /**
+   * Returns a collection of library names for libraries that are added by some framework
+   * and shouldn't be removed during sync. Examples are typescript and dart support.
+   */
+  Collection<String> getExternallyAddedLibraries(BlazeProjectData blazeProjectData);
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java b/blaze-java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
new file mode 100644
index 0000000..06e97e2
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/BlazeJavaSyncPlugin.java
@@ -0,0 +1,363 @@
+/*
+ * 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;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.experiments.BoolExperiment;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.Glob;
+import com.google.idea.blaze.base.projectview.section.SectionParser;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.PerformanceWarning;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.google.idea.blaze.java.projectview.ExcludeLibrarySection;
+import com.google.idea.blaze.java.projectview.ExcludedLibrarySection;
+import com.google.idea.blaze.java.projectview.JavaLanguageLevelSection;
+import com.google.idea.blaze.java.sync.importer.BlazeJavaWorkspaceImporter;
+import com.google.idea.blaze.java.sync.jdeps.JdepsFileReader;
+import com.google.idea.blaze.java.sync.jdeps.JdepsMap;
+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.BlazeLibrary;
+import com.google.idea.blaze.java.sync.projectstructure.Jdks;
+import com.google.idea.blaze.java.sync.projectstructure.LibraryEditor;
+import com.google.idea.blaze.java.sync.projectstructure.SourceFolderEditor;
+import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.module.ModuleType;
+import com.intellij.openapi.module.StdModuleTypes;
+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.ModifiableRootModel;
+import com.intellij.openapi.roots.ex.ProjectRootManagerEx;
+import com.intellij.pom.java.LanguageLevel;
+import com.intellij.util.ui.UIUtil;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Sync support for Java.
+ */
+public class BlazeJavaSyncPlugin extends BlazeSyncPlugin.Adapter {
+  private static final BoolExperiment USE_WORKING_SET = new BoolExperiment("use.working.set", true);
+  private static final Logger LOG = Logger.getInstance(BlazeJavaSyncPlugin.class);
+  private final JdepsFileReader jdepsFileReader = new JdepsFileReader();
+
+  @Nullable
+  @Override
+  public WorkspaceType getDefaultWorkspaceType() {
+    return WorkspaceType.JAVA;
+  }
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.JAVA) {
+      return ImmutableSet.of(LanguageClass.JAVA);
+    }
+    return ImmutableSet.of();
+  }
+
+  @Nullable
+  @Override
+  public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.JAVA) {
+      return StdModuleTypes.JAVA;
+    }
+    return null;
+  }
+
+  @Override
+  public void updateSyncState(Project project,
+                              BlazeContext context,
+                              WorkspaceRoot workspaceRoot,
+                              ProjectViewSet projectViewSet,
+                              WorkspaceLanguageSettings workspaceLanguageSettings,
+                              BlazeRoots blazeRoots,
+                              @Nullable WorkingSet workingSet,
+                              WorkspacePathResolver workspacePathResolver,
+                              ImmutableMap<Label, RuleIdeInfo> ruleMap,
+                              @Deprecated @Nullable File androidPlatformDirectory,
+                              SyncState.Builder syncStateBuilder,
+                              @Nullable SyncState previousSyncState) {
+    JavaWorkingSet javaWorkingSet = null;
+    if (USE_WORKING_SET.getValue() && workingSet != null) {
+      javaWorkingSet = new JavaWorkingSet(workspaceRoot, workingSet);
+    }
+
+    JdepsMap jdepsMap = jdepsFileReader.loadJdepsFiles(context, ruleMap, syncStateBuilder, previousSyncState);
+
+    BlazeJavaWorkspaceImporter blazeJavaWorkspaceImporter = new BlazeJavaWorkspaceImporter(
+      project,
+      workspaceRoot,
+      projectViewSet,
+      ruleMap,
+      jdepsMap,
+      javaWorkingSet,
+      new ArtifactLocationDecoder(blazeRoots, workspacePathResolver)
+    );
+    BlazeJavaImportResult importResult = Scope.push(context, (childContext) -> {
+      childContext.push(new TimingScope("JavaWorkspaceImporter"));
+      return blazeJavaWorkspaceImporter.importWorkspace(childContext);
+    });
+    Glob.GlobSet excludedLibraries = new Glob.GlobSet(
+      ImmutableList.<Glob>builder()
+        .addAll(projectViewSet.listItems(ExcludeLibrarySection.KEY))
+        .addAll(projectViewSet.listItems(ExcludedLibrarySection.KEY))
+        .build()
+    );
+    for (BlazeJavaSyncAugmenter syncAugmenter : BlazeJavaSyncAugmenter.EP_NAME.getExtensions()) {
+      syncAugmenter.addLibraryFilter(excludedLibraries);
+    }
+    BlazeJavaSyncData syncData = new BlazeJavaSyncData(
+      importResult,
+      excludedLibraries,
+      BlazeUserSettings.getInstance().getAttachSourcesByDefault()
+    );
+    syncStateBuilder.put(BlazeJavaSyncData.class, syncData);
+  }
+
+  @Override
+  public void updateSdk(Project project,
+                        BlazeContext context,
+                        ProjectViewSet projectViewSet,
+                        BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.JAVA)) {
+      return;
+    }
+    updateJdk(project, context, projectViewSet, blazeProjectData);
+  }
+
+  @Override
+  public void updateContentEntries(Project project,
+                                   BlazeContext context,
+                                   WorkspaceRoot workspaceRoot,
+                                   ProjectViewSet projectViewSet,
+                                   BlazeProjectData blazeProjectData,
+                                   Collection<ContentEntry> contentEntries) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.JAVA)) {
+      return;
+    }
+    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    if (syncData == null) {
+      return;
+    }
+
+    SourceFolderEditor.modifyContentEntries(
+      syncData.importResult,
+      contentEntries
+    );
+  }
+
+  @Override
+  public void updateProjectStructure(Project project,
+                                     BlazeContext context,
+                                     WorkspaceRoot workspaceRoot,
+                                     ProjectViewSet projectViewSet,
+                                     BlazeProjectData blazeProjectData,
+                                     @Nullable BlazeProjectData oldBlazeProjectData,
+                                     ModuleEditor moduleEditor,
+                                     Module workspaceModule,
+                                     ModifiableRootModel workspaceModifiableModel) {
+    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    if (syncData == null) {
+      return;
+    }
+
+    @Nullable BlazeJavaSyncData oldSyncData = oldBlazeProjectData != null
+                                              ? oldBlazeProjectData.syncState.get(BlazeJavaSyncData.class)
+                                              : null;
+
+    if (syncData.attachSourceJarsByDefault) {
+      context.output(PrintOutput.output(
+        "Attaching source jars by default. This may lead to significant increases in indexing time,"
+        + " project opening time, and IDE memory usage. To turn off go to"
+        + " Settings > Other Settings > Blaze."
+      ));
+    }
+
+    List<BlazeLibrary> newLibraries = getLibraries(blazeProjectData, syncData);
+    final List<BlazeLibrary> oldLibraries;
+    if (oldSyncData != null && oldSyncData.attachSourceJarsByDefault == syncData.attachSourceJarsByDefault) {
+      oldLibraries = getLibraries(oldBlazeProjectData, oldSyncData);
+    } else {
+      oldLibraries = ImmutableList.of();
+    }
+
+    LibraryEditor.updateProjectLibraries(
+      project,
+      context,
+      blazeProjectData,
+      newLibraries,
+      oldLibraries
+    );
+
+    LibraryEditor.configureDependencies(
+      project,
+      context,
+      workspaceModifiableModel,
+      newLibraries
+    );
+  }
+
+  private static List<BlazeLibrary> getLibraries(BlazeProjectData blazeProjectData,
+                                                 BlazeJavaSyncData syncData) {
+    Glob.GlobSet excludedLibraries = syncData.excludedLibraries;
+
+    List<BlazeLibrary> libraries = Lists.newArrayList();
+    libraries.addAll(syncData.importResult.libraries.values());
+    for (BlazeJavaSyncAugmenter syncAugmenter : BlazeJavaSyncAugmenter.EP_NAME.getExtensions()) {
+      libraries.addAll(syncAugmenter.getAdditionalLibraries(blazeProjectData));
+    }
+    return libraries
+      .stream()
+      .filter(blazeLibrary -> !isExcluded(excludedLibraries, blazeLibrary.getLibraryArtifact()))
+      .collect(Collectors.toList());
+  }
+
+  private static boolean isExcluded(Glob.GlobSet excludedLibraries, @Nullable LibraryArtifact libraryArtifact) {
+    if (libraryArtifact == null) {
+      return false;
+    }
+    ArtifactLocation jar = libraryArtifact.jar;
+    ArtifactLocation runtimeJar = libraryArtifact.runtimeJar;
+    return excludedLibraries.matches(jar.getRelativePath())
+           || (runtimeJar != null && excludedLibraries.matches(runtimeJar.getRelativePath()));
+  }
+
+  private static void updateJdk(
+    Project project,
+    BlazeContext context,
+    ProjectViewSet projectViewSet,
+    BlazeProjectData blazeProjectData) {
+
+    LanguageLevel javaLanguageLevel = JavaLanguageLevelHelper
+      .getJavaLanguageLevel(projectViewSet, blazeProjectData, LanguageLevel.JDK_1_7);
+
+    final Sdk sdk = Jdks.chooseOrCreateJavaSdk(javaLanguageLevel);
+    if (sdk == null) {
+      String msg = String.format(
+        "Unable to find a JDK %1$s installed.\n", javaLanguageLevel.getPresentableText());
+      msg += "After configuring a suitable JDK in the \"Project Structure\" dialog, "
+             + "sync the project again.";
+      IssueOutput.error(msg).submit(context);
+      return;
+    }
+    setProjectSdkAndLanguageLevel(project, sdk, javaLanguageLevel);
+  }
+
+  private static void setProjectSdkAndLanguageLevel(
+    final Project project,
+    final Sdk sdk,
+    final LanguageLevel javaLanguageLevel) {
+    UIUtil.invokeAndWaitIfNeeded((Runnable)() -> ApplicationManager.getApplication().runWriteAction(() -> {
+      ProjectRootManagerEx rootManager = ProjectRootManagerEx.getInstanceEx(project);
+      rootManager.setProjectSdk(sdk);
+      LanguageLevelProjectExtension ext = LanguageLevelProjectExtension.getInstance(project);
+      ext.setLanguageLevel(javaLanguageLevel);
+    }));
+  }
+
+  @Override
+  public boolean validate(Project project,
+                          BlazeContext context,
+                          BlazeProjectData blazeProjectData) {
+    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    if (syncData == null) {
+      return true;
+    }
+    warnAboutDeployJars(context, syncData);
+    return true;
+  }
+
+  @Override
+  public Collection<SectionParser> getSections() {
+    return ImmutableList.of(
+      ExcludedLibrarySection.PARSER,
+      ExcludeLibrarySection.PARSER,
+      JavaLanguageLevelSection.PARSER
+    );
+  }
+
+  @Override
+  public boolean requiresResolveIdeArtifacts() {
+    return true;
+  }
+
+  /**
+   * Looks at your jars for anything that seems to be a deploy jar and
+   * warns about it. This often turns out to be a duplicate copy of
+   * all your application's code, so you don't want it in your project.
+   */
+  private static void warnAboutDeployJars(
+    BlazeContext context,
+    BlazeJavaSyncData syncData) {
+    for (BlazeLibrary library : syncData.importResult.libraries.values()) {
+      LibraryArtifact libraryArtifact = library.getLibraryArtifact();
+      if (libraryArtifact == null) {
+        continue;
+      }
+      ArtifactLocation artifactLocation = libraryArtifact.jar;
+      if (artifactLocation.getRelativePath().endsWith("deploy.jar")
+          || artifactLocation.getRelativePath().endsWith("deploy-ijar.jar")
+          || artifactLocation.getRelativePath().endsWith("deploy-hjar.jar")) {
+        context.output(new PerformanceWarning(
+          "Performance warning: You have added a deploy jar as a library. "
+          + "This can lead to poor indexing performance, and the debugger may "
+          + "become confused and step into the deploy jar instead of your code. "
+          + "Consider redoing the rule to not use deploy jars, exclude the target "
+          + "from your .blazeproject, or exclude the library.\n"
+          + "Library path: " + artifactLocation.getRelativePath()
+        ));
+      }
+    }
+  }
+
+  @Override
+  public Set<String> prefetchSrcFileExtensions() {
+    return ImmutableSet.of("java");
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java b/blaze-java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java
new file mode 100644
index 0000000..1120f41
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/DuplicateSourceDetector.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.PerformanceWarning;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Detects and reports duplicate sources
+ */
+public class DuplicateSourceDetector {
+  Multimap<ArtifactLocation, Label> artifacts = ArrayListMultimap.create();
+
+  public void add(Label label, ArtifactLocation artifactLocation) {
+    artifacts.put(artifactLocation, label);
+  }
+
+  static class Duplicate {
+    final ArtifactLocation artifactLocation;
+    final Collection<Label> labels;
+    public Duplicate(ArtifactLocation artifactLocation, Collection<Label> labels) {
+      this.artifactLocation = artifactLocation;
+      this.labels = labels;
+    }
+  }
+
+  public void reportDuplicates(BlazeContext context) {
+    List<Duplicate> duplicates = Lists.newArrayList();
+    for (ArtifactLocation key : artifacts.keySet()) {
+      Collection<Label> labels = artifacts.get(key);
+      if (labels.size() > 1) {
+
+        // Workaround for aspect bug. Can be removed after the next blaze release, as of May 27 2016
+        Set<Label> labelSet = Sets.newHashSet(labels);
+        if (labelSet.size() > 1) {
+          duplicates.add(new Duplicate(key, labelSet));
+        }
+      }
+    }
+
+    if (duplicates.isEmpty()) {
+      return;
+    }
+
+    Collections.sort(duplicates, (lhs, rhs) -> lhs.artifactLocation.getRelativePath().compareTo(rhs.artifactLocation.getRelativePath()));
+
+    context.output(new PerformanceWarning("Duplicate sources detected:"));
+    for (Duplicate duplicate : duplicates) {
+      ArtifactLocation artifactLocation = duplicate.artifactLocation;
+      context.output(new PerformanceWarning("  Source: " + artifactLocation.getRelativePath()));
+      context.output(new PerformanceWarning("  Consumed by rules:"));
+      for (Label label : duplicate.labels) {
+        context.output(new PerformanceWarning("    " + label));
+      }
+      context.output(new PerformanceWarning("")); // Newline
+    }
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/JavaLanguageLevelHelper.java b/blaze-java/src/com/google/idea/blaze/java/sync/JavaLanguageLevelHelper.java
new file mode 100644
index 0000000..8f6bfe4
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/JavaLanguageLevelHelper.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+import com.google.common.base.Strings;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.java.projectview.JavaLanguageLevelSection;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.intellij.pom.java.LanguageLevel;
+
+/**
+ * Called by sync plugins to determine the appropriate java language level.
+ */
+public class JavaLanguageLevelHelper {
+
+  public static LanguageLevel getJavaLanguageLevel(
+    ProjectViewSet projectViewSet,
+    BlazeProjectData blazeProjectData,
+    LanguageLevel defaultLanguageLevel) {
+
+    defaultLanguageLevel = getLanguageLevelFromToolchain(blazeProjectData, defaultLanguageLevel);
+    return JavaLanguageLevelSection.getLanguageLevel(projectViewSet, defaultLanguageLevel);
+  }
+
+  private static LanguageLevel getLanguageLevelFromToolchain(BlazeProjectData blazeProjectData, LanguageLevel defaultLanguageLevel) {
+    BlazeJavaSyncData blazeJavaSyncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    if (blazeJavaSyncData != null) {
+      String sourceVersion = blazeJavaSyncData.importResult.sourceVersion;
+      if (!Strings.isNullOrEmpty(sourceVersion)) {
+        switch (sourceVersion) {
+          case "6":
+            return LanguageLevel.JDK_1_6;
+          case "7":
+            return LanguageLevel.JDK_1_7;
+          case "8":
+            return LanguageLevel.JDK_1_8;
+          case "9":
+            return LanguageLevel.JDK_1_9;
+        }
+      }
+    }
+    return defaultLanguageLevel;
+  }
+
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java b/blaze-java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
new file mode 100644
index 0000000..9868352
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporter.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.importer;
+
+import com.google.common.collect.*;
+import com.google.idea.blaze.base.ideinfo.*;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.ImportRoots;
+import com.google.idea.blaze.base.sync.projectview.ProjectViewRuleImportFilter;
+import com.google.idea.blaze.base.sync.projectview.SourceTestConfig;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.java.sync.DuplicateSourceDetector;
+import com.google.idea.blaze.java.sync.jdeps.JdepsMap;
+import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
+import com.google.idea.blaze.java.sync.model.BlazeJavaImportResult;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import com.google.idea.blaze.java.sync.source.SourceArtifact;
+import com.google.idea.blaze.java.sync.source.SourceDirectoryCalculator;
+import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Builds a BlazeWorkspace.
+ */
+public final class BlazeJavaWorkspaceImporter {
+  private static final Logger LOG = Logger.getInstance(BlazeJavaWorkspaceImporter.class);
+
+  private final Project project;
+  private final WorkspaceRoot workspaceRoot;
+  private final ImportRoots importRoots;
+  private final ImmutableMap<Label, RuleIdeInfo> ruleMap;
+  private final SourceTestConfig sourceTestConfig;
+  private final JdepsMap jdepsMap;
+  @Nullable private final JavaWorkingSet workingSet;
+  private final ArtifactLocationDecoder artifactLocationDecoder;
+  private final ProjectViewRuleImportFilter importFilter;
+  private final DuplicateSourceDetector duplicateSourceDetector = new DuplicateSourceDetector();
+
+  public BlazeJavaWorkspaceImporter(
+    Project project,
+    WorkspaceRoot workspaceRoot,
+    ProjectViewSet projectViewSet,
+    ImmutableMap<Label, RuleIdeInfo> ruleMap,
+    JdepsMap jdepsMap,
+    @Nullable JavaWorkingSet workingSet,
+    ArtifactLocationDecoder artifactLocationDecoder) {
+    this.project = project;
+    this.workspaceRoot = workspaceRoot;
+    this.importRoots = ImportRoots.builder(workspaceRoot, Blaze.getBuildSystem(project))
+      .add(projectViewSet)
+      .build();
+    this.ruleMap = ruleMap;
+    this.jdepsMap = jdepsMap;
+    this.workingSet = workingSet;
+    this.artifactLocationDecoder = artifactLocationDecoder;
+    this.importFilter = new ProjectViewRuleImportFilter(project, workspaceRoot, projectViewSet);
+    this.sourceTestConfig = new SourceTestConfig(projectViewSet);
+  }
+
+  public BlazeJavaImportResult importWorkspace(BlazeContext context) {
+    List<RuleIdeInfo> includedRules = ruleMap.values().stream()
+      .filter(rule -> !importFilter.excludeTarget(rule))
+      .collect(Collectors.toList());
+
+    List<RuleIdeInfo> javaRules = includedRules.stream()
+      .filter(rule -> rule.javaRuleIdeInfo != null)
+      .collect(Collectors.toList());
+
+    List<RuleIdeInfo> sourceRules = Lists.newArrayList();
+    List<RuleIdeInfo> libraryRules = Lists.newArrayList();
+    for (RuleIdeInfo rule : javaRules) {
+      boolean importAsSource =
+        importFilter.isSourceRule(rule)
+        && canImportAsSource(rule)
+        && !allSourcesGenerated(rule);
+
+      if (importAsSource) {
+        sourceRules.add(rule);
+      } else {
+        libraryRules.add(rule);
+      }
+    }
+
+    List<RuleIdeInfo> protoLibraries = includedRules.stream()
+      .filter(rule -> rule.kind == Kind.PROTO_LIBRARY)
+      .collect(Collectors.toList());
+
+    WorkspaceBuilder workspaceBuilder = new WorkspaceBuilder();
+    for (RuleIdeInfo rule : sourceRules) {
+      addRuleAsSource(workspaceBuilder, rule);
+    }
+
+    SourceDirectoryCalculator sourceDirectoryCalculator = new SourceDirectoryCalculator();
+    ImmutableList<BlazeContentEntry> contentEntries = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      sourceTestConfig,
+      artifactLocationDecoder,
+      importRoots.rootDirectories(),
+      workspaceBuilder.sourceArtifacts,
+      workspaceBuilder.javaPackageManifests
+    );
+
+    int totalContentEntryCount = 0;
+    for (BlazeContentEntry contentEntry : contentEntries) {
+      totalContentEntryCount += contentEntry.sources.size();
+    }
+    context.output(PrintOutput.output("Java content entry count: " + totalContentEntryCount));
+
+    ImmutableMap<LibraryKey, BlazeLibrary> libraries = buildLibraries(workspaceBuilder, ruleMap, libraryRules, protoLibraries);
+
+    duplicateSourceDetector.reportDuplicates(context);
+
+    String sourceVersion = findSourceVersion(ruleMap);
+
+    return new BlazeJavaImportResult(
+      contentEntries,
+      libraries,
+      ImmutableList.copyOf(workspaceBuilder.buildOutputJars
+                             .stream()
+                             .sorted()
+                             .collect(Collectors.toList())),
+      ImmutableSet.copyOf(workspaceBuilder.addedSourceFiles),
+      sourceVersion
+    );
+  }
+
+  private boolean canImportAsSource(RuleIdeInfo rule) {
+    return !rule.kindIsOneOf(Kind.JAVA_WRAP_CC, Kind.JAVA_IMPORT);
+  }
+
+  private boolean allSourcesGenerated(RuleIdeInfo rule) {
+    return !rule.sources.isEmpty() && rule.sources.stream().allMatch(ArtifactLocation::isGenerated);
+  }
+
+  private ImmutableMap<LibraryKey, BlazeLibrary> buildLibraries(WorkspaceBuilder workspaceBuilder,
+                                                                Map<Label, RuleIdeInfo> ruleMap,
+                                                                List<RuleIdeInfo> libraryRules,
+                                                                List<RuleIdeInfo> protoLibraries) {
+    // Build library maps
+    Multimap<Label, LibraryArtifact> labelToLibrary = ArrayListMultimap.create();
+    Map<String, LibraryArtifact> jdepsPathToLibrary = Maps.newHashMap();
+    for (RuleIdeInfo rule : libraryRules) {
+      JavaRuleIdeInfo javaRuleIdeInfo = rule.javaRuleIdeInfo;
+      if (javaRuleIdeInfo == null) {
+        continue;
+      }
+      Iterable<LibraryArtifact> libraries = Iterables.concat(javaRuleIdeInfo.jars, javaRuleIdeInfo.generatedJars);
+      labelToLibrary.putAll(rule.label, libraries);
+      for (LibraryArtifact libraryArtifact : libraries) {
+        addLibraryToJdeps(jdepsPathToLibrary, libraryArtifact);
+      }
+    }
+
+    // proto legacy jdeps support
+    for (RuleIdeInfo rule : protoLibraries) {
+      ProtoLibraryLegacyInfo protoLibraryLegacyInfo = rule.protoLibraryLegacyInfo;
+      if (protoLibraryLegacyInfo == null) {
+        continue;
+      }
+      for (LibraryArtifact libraryArtifact : Iterables.concat(protoLibraryLegacyInfo.jarsV1,
+                                                              protoLibraryLegacyInfo.jarsMutable,
+                                                              protoLibraryLegacyInfo.jarsImmutable)) {
+        addLibraryToJdeps(jdepsPathToLibrary, libraryArtifact);
+      }
+    }
+
+    // Collect jars from jdep references
+    Set<LibraryArtifact> libraries = Sets.newHashSet();
+    for (String jdepsPath : workspaceBuilder.jdeps) {
+      LibraryArtifact libraryArtifact = jdepsPathToLibrary.get(jdepsPath);
+      if (libraryArtifact != null) {
+        libraries.add(libraryArtifact);
+      }
+    }
+
+    // Collect jars referenced by direct deps from your working set
+    for (Label deps : workspaceBuilder.directDeps) {
+      libraries.addAll(labelToLibrary.get(deps));
+    }
+
+    // Collect legacy proto libraries from direct deps
+    addProtoLegacyLibrariesFromDirectDeps(workspaceBuilder, ruleMap, libraries);
+
+    // Collect generated jars from source rules
+    libraries.addAll(workspaceBuilder.generatedJars);
+
+    ImmutableMap.Builder<LibraryKey, BlazeLibrary> result = ImmutableMap.builder();
+    for (LibraryArtifact libraryArtifact : libraries) {
+      File jar = libraryArtifact.jar.getFile();
+      LibraryKey key = LibraryKey.fromJarFile(jar);
+      BlazeLibrary blazeLibrary = new BlazeLibrary(key, libraryArtifact);
+      result.put(key, blazeLibrary);
+    }
+    return result.build();
+  }
+
+  private void addProtoLegacyLibrariesFromDirectDeps(WorkspaceBuilder workspaceBuilder,
+                                                     Map<Label, RuleIdeInfo> ruleMap,
+                                                     Set<LibraryArtifact> result) {
+    List<Label> version1Roots = Lists.newArrayList();
+    List<Label> immutableRoots = Lists.newArrayList();
+    List<Label> mutableRoots = Lists.newArrayList();
+    for (Label label : workspaceBuilder.directDeps) {
+      RuleIdeInfo rule = ruleMap.get(label);
+      if (rule == null) {
+        continue;
+      }
+      ProtoLibraryLegacyInfo protoLibraryLegacyInfo = rule.protoLibraryLegacyInfo;
+      if (protoLibraryLegacyInfo == null) {
+        continue;
+      }
+      switch (protoLibraryLegacyInfo.apiFlavor) {
+        case VERSION_1:
+          version1Roots.add(label);
+          break;
+        case IMMUTABLE:
+          immutableRoots.add(label);
+          break;
+        case MUTABLE:
+          mutableRoots.add(label);
+          break;
+        case BOTH:
+          mutableRoots.add(label);
+          immutableRoots.add(label);
+          break;
+        default:
+          // Can't happen
+          break;
+      }
+    }
+
+    addProtoLegacyLibrariesFromDirectDepsForFlavor(ruleMap, ProtoLibraryLegacyInfo.ApiFlavor.VERSION_1, version1Roots, result);
+    addProtoLegacyLibrariesFromDirectDepsForFlavor(ruleMap, ProtoLibraryLegacyInfo.ApiFlavor.IMMUTABLE, immutableRoots, result);
+    addProtoLegacyLibrariesFromDirectDepsForFlavor(ruleMap, ProtoLibraryLegacyInfo.ApiFlavor.MUTABLE, mutableRoots, result);
+  }
+
+  private void addProtoLegacyLibrariesFromDirectDepsForFlavor(Map<Label, RuleIdeInfo> ruleMap,
+                                                              ProtoLibraryLegacyInfo.ApiFlavor apiFlavor,
+                                                              List<Label> roots,
+                                                              Set<LibraryArtifact> result) {
+    Set<Label> seen = Sets.newHashSet();
+    while (!roots.isEmpty()) {
+      Label label = roots.remove(roots.size() - 1);
+      if (!seen.add(label)) {
+        continue;
+      }
+      RuleIdeInfo rule = ruleMap.get(label);
+      if (rule == null) {
+        continue;
+      }
+      ProtoLibraryLegacyInfo protoLibraryLegacyInfo = rule.protoLibraryLegacyInfo;
+      if (protoLibraryLegacyInfo == null) {
+        continue;
+      }
+      switch (apiFlavor) {
+        case VERSION_1:
+          result.addAll(protoLibraryLegacyInfo.jarsV1);
+          break;
+        case MUTABLE:
+          result.addAll(protoLibraryLegacyInfo.jarsMutable);
+          break;
+        case IMMUTABLE:
+          result.addAll(protoLibraryLegacyInfo.jarsImmutable);
+          break;
+        default:
+          // Can't happen
+          break;
+      }
+
+      roots.addAll(rule.dependencies);
+    }
+  }
+
+  private void addLibraryToJdeps(Map<String, LibraryArtifact> jdepsPathToLibrary, LibraryArtifact libraryArtifact) {
+    ArtifactLocation jar = libraryArtifact.jar;
+    jdepsPathToLibrary.put(jar.getExecutionRootRelativePath(), libraryArtifact);
+    ArtifactLocation runtimeJar = libraryArtifact.runtimeJar;
+    if (runtimeJar != null) {
+      jdepsPathToLibrary.put(runtimeJar.getExecutionRootRelativePath(), libraryArtifact);
+    }
+  }
+
+  private void addRuleAsSource(
+    WorkspaceBuilder workspaceBuilder,
+    RuleIdeInfo rule) {
+    JavaRuleIdeInfo javaRuleIdeInfo = rule.javaRuleIdeInfo;
+    if (javaRuleIdeInfo == null) {
+      return;
+    }
+
+    Collection<String> jars = jdepsMap.getDependenciesForRule(rule.label);
+    if (jars != null) {
+      workspaceBuilder.jdeps.addAll(jars);
+    }
+
+    // Add all deps if this rule is in the current working set
+    if (workingSet == null || workingSet.isRuleInWorkingSet(rule)) {
+      workspaceBuilder.directDeps.addAll(rule.dependencies);
+    }
+
+    for (ArtifactLocation artifactLocation : rule.sources) {
+      if (!artifactLocation.isGenerated()) {
+        duplicateSourceDetector.add(rule.label, artifactLocation);
+        workspaceBuilder.sourceArtifacts.add(new SourceArtifact(rule.label, artifactLocation));
+        workspaceBuilder.addedSourceFiles.add(artifactLocation.getFile());
+      }
+    }
+
+    ArtifactLocation manifest = javaRuleIdeInfo.packageManifest;
+    if (manifest != null) {
+      workspaceBuilder.javaPackageManifests.put(rule.label, manifest);
+    }
+    for (LibraryArtifact libraryArtifact : javaRuleIdeInfo.jars) {
+      ArtifactLocation runtimeJar = libraryArtifact.runtimeJar;
+      if (runtimeJar != null) {
+        workspaceBuilder.buildOutputJars.add(runtimeJar.getFile());
+      }
+    }
+    workspaceBuilder.generatedJars.addAll(javaRuleIdeInfo.generatedJars);
+  }
+
+  @Nullable
+  private String findSourceVersion(ImmutableMap<Label, RuleIdeInfo> ruleMap) {
+    for (RuleIdeInfo rule : ruleMap.values()) {
+      if (rule.javaToolchainIdeInfo != null) {
+        return rule.javaToolchainIdeInfo.sourceVersion;
+      }
+    }
+    return null;
+  }
+
+  static class WorkspaceBuilder {
+    Set<String> jdeps = Sets.newHashSet();
+    Set<Label> directDeps = Sets.newHashSet();
+    Set<File> addedSourceFiles = Sets.newHashSet();
+    List<LibraryArtifact> generatedJars = Lists.newArrayList();
+    List<File> buildOutputJars = Lists.newArrayList();
+    List<SourceArtifact> sourceArtifacts = Lists.newArrayList();
+    Map<Label, ArtifactLocation> javaPackageManifests = Maps.newHashMap();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/importer/LibraryBuilder.java b/blaze-java/src/com/google/idea/blaze/java/sync/importer/LibraryBuilder.java
new file mode 100644
index 0000000..0d660f5
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/importer/LibraryBuilder.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.importer;
+
+import com.google.common.collect.*;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Manages libraries during the module import stage.
+ */
+public class LibraryBuilder {
+
+  private final Map<LibraryKey, BlazeLibrary> libraries = Maps.newHashMap();
+  private final Multimap<Label, LibraryKey> labelToLibraryKeys = ArrayListMultimap.create();
+  private final Map<String, LibraryKey> jdepsPathToLibraryKey = Maps.newHashMap();
+  private final Set<LibraryKey> referencedLibraryKeys = Sets.newHashSet();
+
+  void createLibraryForRule(Label label, LibraryArtifact libraryArtifact) {
+    LibraryKey libraryKey = createLibrary(libraryArtifact);
+    labelToLibraryKeys.put(label, libraryKey);
+  }
+
+  public void createLibraryForModule(LibraryArtifact libraryArtifact) {
+    LibraryKey libraryKey = createLibrary(libraryArtifact);
+    referencedLibraryKeys.add(libraryKey);
+  }
+
+  void referenceLibraryFromModule(Label label) {
+    Collection<LibraryKey> libraryKeys = labelToLibraryKeys.get(label);
+    referencedLibraryKeys.addAll(libraryKeys);
+  }
+
+  void referenceLibraryFromModule(String jdepsPath) {
+    LibraryKey libraryKey = jdepsPathToLibraryKey.get(jdepsPath);
+    if (libraryKey != null) {
+      referencedLibraryKeys.add(libraryKey);
+    }
+  }
+
+  private LibraryKey createLibrary(LibraryArtifact libraryArtifact) {
+    File jar = libraryArtifact.jar.getFile();
+    LibraryKey key = LibraryKey.fromJarFile(jar);
+    BlazeLibrary library = new BlazeLibrary(key, libraryArtifact);
+    addLibrary(key, library);
+    return key;
+  }
+
+  private void addLibrary(LibraryKey key,
+                          BlazeLibrary library) {
+    BlazeLibrary existingLibrary = libraries.putIfAbsent(key, library);
+    existingLibrary = existingLibrary != null ? existingLibrary : library;
+
+    LibraryArtifact libraryArtifact = existingLibrary.getLibraryArtifact();
+
+    // Index the library by jar for jdeps support
+    if (libraryArtifact != null) {
+      ArtifactLocation jar = libraryArtifact.jar;
+      jdepsPathToLibraryKey.put(jar.getExecutionRootRelativePath(), key);
+
+      ArtifactLocation runtimeJar = libraryArtifact.runtimeJar;
+      if (runtimeJar != null) {
+        jdepsPathToLibraryKey.put(runtimeJar.getExecutionRootRelativePath(), key);
+      }
+    }
+  }
+
+  ImmutableMap<LibraryKey, BlazeLibrary> build() {
+    ImmutableMap.Builder<LibraryKey, BlazeLibrary> result = ImmutableMap.builder();
+    for (LibraryKey libraryKey : referencedLibraryKeys) {
+      result.put(libraryKey, libraries.get(libraryKey));
+    }
+    return result.build();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java b/blaze-java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java
new file mode 100644
index 0000000..c7e1333
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/jdeps/JdepsFileReader.java
@@ -0,0 +1,176 @@
+/*
+ * 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.jdeps;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.FutureUtil;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.JavaRuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.prefetch.PrefetchService;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.sync.filediff.FileDiffService;
+import com.google.repackaged.devtools.build.lib.view.proto.Deps;
+import com.intellij.openapi.diagnostic.Logger;
+
+import javax.annotation.Nullable;
+import java.io.*;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * Reads jdeps from the ide info result.
+ */
+public class JdepsFileReader {
+  private static final Logger LOG = Logger.getInstance(JdepsFileReader.class);
+  private final FileDiffService fileDiffService = new FileDiffService();
+
+  static class JdepsState implements Serializable {
+    private static final long serialVersionUID = 2L;
+    private FileDiffService.State fileState = null;
+    private Map<File, Label> fileToLabelMap = Maps.newHashMap();
+    private Map<Label, List<String>> labelToJdeps = Maps.newHashMap();
+  }
+
+  private static class Result {
+    File file;
+    Label label;
+    List<String> dependencies;
+    public Result(File file, Label label, List<String> dependencies) {
+      this.file = file;
+      this.label = label;
+      this.dependencies = dependencies;
+    }
+  }
+
+  /**
+   * Loads any updated jdeps files since the last invocation of this method.
+   */
+  @Nullable
+  public JdepsMap loadJdepsFiles(BlazeContext parentContext,
+                                 ImmutableMap<Label, RuleIdeInfo> ruleMap,
+                                 SyncState.Builder syncStateBuilder,
+                                 @Nullable SyncState previousSyncState) {
+    JdepsState oldState = previousSyncState != null ? previousSyncState.get(JdepsState.class) : null;
+    JdepsState jdepsState = Scope.push(parentContext, (context) -> {
+      context.push(new TimingScope("LoadJdepsFiles"));
+      return doLoadJdepsFiles(context, oldState, ruleMap);
+    });
+    if (jdepsState == null) {
+      return null;
+    }
+    syncStateBuilder.put(JdepsState.class, jdepsState);
+    return label -> jdepsState.labelToJdeps.get(label);
+  }
+
+  private JdepsState doLoadJdepsFiles(BlazeContext context,
+                                      @Nullable JdepsState oldState,
+                                      ImmutableMap<Label, RuleIdeInfo> ruleMap) {
+    JdepsState state = new JdepsState();
+    if (oldState != null) {
+      state.labelToJdeps = Maps.newHashMap(oldState.labelToJdeps);
+      state.fileToLabelMap = Maps.newHashMap(oldState.fileToLabelMap);
+    }
+
+    List<File> files = Lists.newArrayList();
+    for (RuleIdeInfo ruleIdeInfo : ruleMap.values()) {
+      JavaRuleIdeInfo javaRuleIdeInfo = ruleIdeInfo.javaRuleIdeInfo;
+      if (javaRuleIdeInfo != null) {
+        ArtifactLocation jdepsFile = javaRuleIdeInfo.jdepsFile;
+        if (jdepsFile != null) {
+          files.add(jdepsFile.getFile());
+        }
+      }
+    }
+
+    List<File> updatedFiles = Lists.newArrayList();
+    List<File> removedFiles = Lists.newArrayList();
+    state.fileState = fileDiffService.updateFiles(oldState != null ? oldState.fileState : null, files, updatedFiles, removedFiles);
+
+    ListenableFuture<?> fetchFuture = PrefetchService.getInstance().prefetchFiles(updatedFiles, true);
+    if (!FutureUtil.waitForFuture(context, fetchFuture)
+      .timed("FetchJdeps")
+      .run()
+      .success()) {
+      return null;
+    }
+
+    for (File removedFile : removedFiles) {
+      Label label = state.fileToLabelMap.remove(removedFile);
+      if (label != null) {
+        state.labelToJdeps.remove(label);
+      }
+    }
+
+    AtomicLong totalSizeLoaded = new AtomicLong(0);
+
+    List<ListenableFuture<Result>> futures = Lists.newArrayList();
+    for (File updatedFile : updatedFiles) {
+      futures.add(submit(() -> {
+        totalSizeLoaded.addAndGet(updatedFile.length());
+        try (InputStream inputStream = new FileInputStream(updatedFile)){
+          Deps.Dependencies dependencies = Deps.Dependencies.parseFrom(inputStream);
+          if (dependencies != null) {
+            if (dependencies.hasRuleLabel()) {
+              Label label = new Label(dependencies.getRuleLabel());
+              List<String> dependencyStringList = Lists.newArrayList();
+              for (Deps.Dependency dependency : dependencies.getDependencyList()) {
+                dependencyStringList.add(dependency.getPath());
+              }
+              return new Result(updatedFile, label, dependencyStringList);
+            }
+          }
+        } catch (FileNotFoundException e) {
+          LOG.info("Could not open jdeps file: " + updatedFile);
+        }
+        return null;
+      }));
+    }
+    try {
+      for (Result result : Futures.allAsList(futures).get()) {
+        if (result != null) {
+          state.fileToLabelMap.put(result.file, result.label);
+          state.labelToJdeps.put(result.label, result.dependencies);
+        }
+      }
+      context.output(new PrintOutput(String.format(
+        "Loaded %d jdeps files, total size %dkB", updatedFiles.size(), totalSizeLoaded.get() / 1024
+      )));
+    }
+    catch (InterruptedException | ExecutionException e) {
+      LOG.error(e);
+      return null;
+    }
+    return state;
+  }
+
+  private static <T> ListenableFuture<T> submit(Callable<T> callable) {
+    return BlazeExecutor.getInstance().submit(callable);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java b/blaze-java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
new file mode 100644
index 0000000..9d7b301
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/jdeps/JdepsMap.java
@@ -0,0 +1,39 @@
+/*
+ * 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.jdeps;
+
+import com.google.idea.blaze.base.model.primitives.Label;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+/**
+ * Map of rule -> jdeps dependencies.
+ */
+public interface JdepsMap {
+  /**
+   * For a given rule, returns workspace root relative paths of artifacts that
+   * were used during compilation.
+   *
+   * It's not specified whether jars or ijars are used during compilation.
+   *
+   * If the rule doesn't have source or otherwise wasn't instrumented,
+   * null is returned.
+   */
+  @Nullable
+  List<String> getDependenciesForRule(@NotNull Label label);
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeContentEntry.java b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeContentEntry.java
new file mode 100644
index 0000000..ebb4d70
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeContentEntry.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.model;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.File;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Corresponds to an IntelliJ content entry.
+ */
+@Immutable
+public class BlazeContentEntry implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final File contentRoot;
+  public final ImmutableList<BlazeSourceDirectory> sources;
+
+  public BlazeContentEntry(File contentRoot,
+                           ImmutableList<BlazeSourceDirectory> sources) {
+    this.contentRoot = contentRoot;
+    this.sources = sources;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    BlazeContentEntry that = (BlazeContentEntry)o;
+    return Objects.equal(contentRoot, that.contentRoot) &&
+           Objects.equal(sources, that.sources);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(contentRoot, sources);
+  }
+
+  @Override
+  public String toString() {
+    return "BlazeContentEntry {\n"
+           + "  contentRoot: " + contentRoot + "\n"
+           + "  sources: " + sources + "\n"
+           + '}';
+  }
+
+  public static Builder builder(String contentRoot) {
+    return new Builder(new File(contentRoot));
+  }
+
+  public static Builder builder(File contentRoot) {
+    return new Builder(contentRoot);
+  }
+
+  public static class Builder {
+    File contentRoot;
+    List<BlazeSourceDirectory> sources = Lists.newArrayList();
+    public Builder(File contentRoot) {
+      this.contentRoot = contentRoot;
+    }
+    public Builder addSource(BlazeSourceDirectory sourceDirectory) {
+      this.sources.add(sourceDirectory);
+      return this;
+    }
+    public BlazeContentEntry build() {
+      Collections.sort(sources, BlazeSourceDirectory.COMPARATOR);
+      return new BlazeContentEntry(contentRoot, ImmutableList.copyOf(sources));
+    }
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeJavaImportResult.java b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeJavaImportResult.java
new file mode 100644
index 0000000..ac43b94
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeJavaImportResult.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.java.sync.model;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import java.io.File;
+import java.io.Serializable;
+
+/**
+ * The result of a blaze import operation.
+ */
+@Immutable
+public class BlazeJavaImportResult implements Serializable {
+  private static final long serialVersionUID = 3L;
+
+  public final ImmutableList<BlazeContentEntry> contentEntries;
+  public final ImmutableMap<LibraryKey, BlazeLibrary> libraries;
+  public final ImmutableCollection<File> buildOutputJars;
+  public final ImmutableSet<File> javaSourceFiles;
+  @Nullable public final String sourceVersion;
+
+  public BlazeJavaImportResult(ImmutableList<BlazeContentEntry> contentEntries,
+                               ImmutableMap<LibraryKey, BlazeLibrary> libraries,
+                               ImmutableCollection<File> buildOutputJars,
+                               ImmutableSet<File> javaSourceFiles,
+                               @Nullable String sourceVersion) {
+    this.contentEntries = contentEntries;
+    this.libraries = libraries;
+    this.buildOutputJars = buildOutputJars;
+    this.javaSourceFiles = javaSourceFiles;
+    this.sourceVersion = sourceVersion;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeJavaSyncData.java b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeJavaSyncData.java
new file mode 100644
index 0000000..44367b9
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeJavaSyncData.java
@@ -0,0 +1,39 @@
+/*
+ * 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.model;
+
+import com.google.idea.blaze.base.projectview.section.Glob;
+
+import java.io.Serializable;
+
+/**
+ * Sync data for the java plugin.
+ */
+public class BlazeJavaSyncData implements Serializable {
+  private static final long serialVersionUID = 2L;
+
+  public final BlazeJavaImportResult importResult;
+  public final Glob.GlobSet excludedLibraries;
+  public final boolean attachSourceJarsByDefault;
+
+  public BlazeJavaSyncData(BlazeJavaImportResult importResult,
+                           Glob.GlobSet excludedLibraries,
+                           boolean attachSourceJarsByDefault) {
+    this.importResult = importResult;
+    this.excludedLibraries = excludedLibraries;
+    this.attachSourceJarsByDefault = attachSourceJarsByDefault;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeLibrary.java b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeLibrary.java
new file mode 100644
index 0000000..a656a1b
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeLibrary.java
@@ -0,0 +1,109 @@
+/*
+ * 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.model;
+
+import com.google.common.base.Objects;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.File;
+import java.io.Serializable;
+import java.util.Collection;
+
+/**
+ * An immutable reference to a .jar required by a rule. This class supports value semantics when
+ * used as a key to a hash map.
+ */
+@Immutable
+public final class BlazeLibrary implements Serializable {
+  private static final long serialVersionUID = 6L;
+
+  @NotNull
+  private final LibraryKey key;
+
+  @Nullable
+  private final LibraryArtifact libraryArtifact;
+
+  @Nullable
+  private final Collection<File> sources;
+
+  public BlazeLibrary(
+    @NotNull LibraryKey key,
+    @NotNull LibraryArtifact libraryArtifact) {
+    this(key, libraryArtifact, null);
+  }
+
+  public BlazeLibrary(
+    @NotNull LibraryKey key,
+    @NotNull Collection<File> sources) {
+    this(key, null, sources);
+  }
+
+  private BlazeLibrary(
+    @NotNull LibraryKey key,
+    @Nullable LibraryArtifact libraryArtifact,
+    @Nullable Collection<File> sources) {
+    this.key = key;
+    this.libraryArtifact = libraryArtifact;
+    this.sources = sources;
+  }
+
+  /**
+   * Returns the library key.
+   */
+  @NotNull
+  public LibraryKey getKey() {
+    return key;
+  }
+
+  @Nullable
+  public LibraryArtifact getLibraryArtifact() {
+    return libraryArtifact;
+  }
+
+  @Nullable
+  public Collection<File> getSources() {
+    return sources;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(key, libraryArtifact, sources);
+  }
+
+  @Override
+  public String toString() {
+    return key.toString();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (!(other instanceof BlazeLibrary)) {
+      return false;
+    }
+
+    BlazeLibrary that = (BlazeLibrary)other;
+
+    return Objects.equal(key, that.key)
+           && Objects.equal(libraryArtifact, that.libraryArtifact)
+           && Objects.equal(sources, that.sources);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeSourceDirectory.java b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeSourceDirectory.java
new file mode 100644
index 0000000..f0b89d5
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/model/BlazeSourceDirectory.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.model;
+
+import com.google.common.base.Objects;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.File;
+import java.io.Serializable;
+import java.util.Comparator;
+
+/**
+ * A source directory.
+ */
+@Immutable
+public final class BlazeSourceDirectory implements Serializable {
+  private static final long serialVersionUID = 2L;
+
+  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 boolean isGenerated;
+  private final boolean isResource;
+  @NotNull
+  private final String packagePrefix;
+
+  public static class Builder {
+    @NotNull private final File directory;
+    @NotNull private String packagePrefix = "";
+    private boolean isTest;
+    private boolean isResource;
+    private boolean isGenerated;
+
+    private Builder(@NotNull File directory) {
+      this.directory = directory;
+    }
+    public Builder setPackagePrefix(@NotNull 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;
+    }
+    public Builder setGenerated(boolean isGenerated) {
+      this.isGenerated = isGenerated;
+      return this;
+    }
+
+    public BlazeSourceDirectory build() {
+      return new BlazeSourceDirectory(directory, isTest, isResource, isGenerated, packagePrefix);
+    }
+  }
+
+  @NotNull
+  public static Builder builder(@NotNull String directory) {
+    return new Builder(new File(directory));
+  }
+
+  @NotNull
+  public static Builder builder(@NotNull File directory) {
+    return new Builder(directory);
+  }
+
+  private BlazeSourceDirectory(
+    @NotNull File directory,
+    boolean isTest,
+    boolean isResource,
+    boolean isGenerated,
+    @NotNull 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;
+  }
+
+  /**
+   * Returns {@code true} if the directory contains generated files.
+   */
+  public boolean getIsGenerated() {
+    return isGenerated;
+  }
+
+  /**
+   * 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);
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (this == other) {
+      return true;
+    }
+    if (!(other instanceof BlazeSourceDirectory)) {
+      return false;
+    }
+    BlazeSourceDirectory that = (BlazeSourceDirectory)other;
+    return directory.equals(that.directory)
+           && packagePrefix.equals(that.packagePrefix)
+           && isResource == that.isResource
+           && isTest == that.isTest
+           && isGenerated == that.isGenerated;
+  }
+
+  @Override
+  public String toString() {
+    return "BlazeSourceDirectory {\n"
+           + "  directory: " + directory + "\n"
+           + "  isTest: " + isTest + "\n"
+           + "  isGenerated: " + isGenerated + "\n"
+           + "  isResource: " + isResource + "\n"
+           + "  packagePrefix: " + packagePrefix + "\n"
+           + '}';
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/model/LibraryKey.java b/blaze-java/src/com/google/idea/blaze/java/sync/model/LibraryKey.java
new file mode 100644
index 0000000..1ab31b5
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/model/LibraryKey.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.model;
+
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.util.io.FileUtil;
+import org.jetbrains.annotations.NotNull;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.File;
+import java.io.Serializable;
+import java.util.Comparator;
+
+/**
+ * Uniquely identifies a library as imported into IntellJ.
+ */
+@Immutable
+public final class LibraryKey implements Serializable {
+  public static final long serialVersionUID = 1L;
+
+  public static final Comparator<LibraryKey> COMPARATOR = new Comparator<LibraryKey>() {
+    @Override
+    public int compare(LibraryKey o1, LibraryKey o2) {
+      return String.CASE_INSENSITIVE_ORDER.compare(o1.name, o2.name);
+    }
+  };
+
+  @NotNull
+  private final String name;
+
+  @NotNull
+  public static LibraryKey fromJarFile(@NotNull File jarFile) {
+    int parentHash = jarFile.getParent().hashCode();
+    String name = FileUtil.getNameWithoutExtension(jarFile) + "_" + Integer.toHexString(parentHash);
+    return new LibraryKey(name);
+  }
+
+  @NotNull
+  public static LibraryKey forResourceLibrary() {
+    return new LibraryKey("external_resources_library");
+  }
+
+  @NotNull
+  public static LibraryKey fromIntelliJLibrary(@NotNull Library library) {
+    String name = library.getName();
+    if (name == null) {
+      throw new IllegalArgumentException("Null library name");
+    }
+    return fromIntelliJLibraryName(name);
+  }
+
+  @NotNull
+  public static LibraryKey fromIntelliJLibraryName(@NotNull String name) {
+    return new LibraryKey(name);
+  }
+
+  LibraryKey(@NotNull String name) {
+    this.name = name;
+  }
+
+  public String getIntelliJLibraryName() {
+    return name;
+  }
+
+  @Override
+  public String toString() {
+    return name;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    LibraryKey that = (LibraryKey)o;
+    return name.equals(that.name);
+  }
+
+  @Override
+  public int hashCode() {
+    return name.hashCode();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java b/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
new file mode 100644
index 0000000..c63b1dd
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/Jdks.java
@@ -0,0 +1,206 @@
+/*
+ * 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.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.sync.sdk.DefaultSdkProvider;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.projectRoots.JavaSdk;
+import com.intellij.openapi.projectRoots.JavaSdkVersion;
+import com.intellij.openapi.projectRoots.ProjectJdkTable;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.pom.java.LanguageLevel;
+import com.intellij.util.SystemProperties;
+import org.jetbrains.annotations.NonNls;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil.createAndAddSDK;
+import static com.intellij.openapi.util.io.FileUtil.notNullize;
+import static com.intellij.openapi.util.text.StringUtil.isNotEmpty;
+import static java.util.Collections.emptyList;
+
+/**
+ * Utility methods related to IDEA JDKs.
+ */
+public class Jdks {
+  @NonNls
+  private static final LanguageLevel DEFAULT_LANG_LEVEL = LanguageLevel.JDK_1_7;
+
+  @Nullable
+  public static Sdk chooseOrCreateJavaSdk(LanguageLevel langLevel) {
+    for (Sdk sdk : ProjectJdkTable.getInstance().getAllJdks()) {
+      if (isApplicableJdk(sdk, langLevel)) {
+        return sdk;
+      }
+    }
+    String jdkHomePath = null;
+    for (DefaultSdkProvider defaultSdkProvider : DefaultSdkProvider.EP_NAME.getExtensions()) {
+      File sdk = defaultSdkProvider.provideSdkForLanguage(LanguageClass.JAVA);
+      if (sdk != null) {
+        jdkHomePath = sdk.getPath();
+        break;
+      }
+    }
+
+    if (jdkHomePath == null) {
+      jdkHomePath = getJdkHomePath(langLevel);
+    }
+
+    if (jdkHomePath == null) {
+      return null;
+    }
+
+    return createJdk(jdkHomePath);
+  }
+
+  public static boolean isApplicableJdk(@NotNull Sdk jdk, @Nullable LanguageLevel langLevel) {
+    if (!(jdk.getSdkType() instanceof JavaSdk)) {
+      return false;
+    }
+    if (langLevel == null) {
+      langLevel = DEFAULT_LANG_LEVEL;
+    }
+    JavaSdkVersion version = JavaSdk.getInstance().getVersion(jdk);
+    if (version != null) {
+      //noinspection TestOnlyProblems
+      return hasMatchingLangLevel(version, langLevel);
+    }
+    return false;
+  }
+
+  @Nullable
+  public static String getJdkHomePath(@NotNull LanguageLevel langLevel) {
+    Collection<String> jdkHomePaths = new ArrayList<String>(JavaSdk.getInstance().suggestHomePaths());
+    if (jdkHomePaths.isEmpty()) {
+      return null;
+    }
+    // prefer jdk path of getJavaHome(), since we have to allow access to it in tests
+    // see AndroidProjectDataServiceTest#testImportData()
+    final List<String> list = new ArrayList<String>();
+    String javaHome = SystemProperties.getJavaHome();
+
+    if (javaHome != null && !javaHome.isEmpty()) {
+      for (Iterator<String> it = jdkHomePaths.iterator(); it.hasNext(); ) {
+        final String path = it.next();
+
+        if (path != null && javaHome.startsWith(path)) {
+          it.remove();
+          list.add(path);
+        }
+      }
+    }
+    list.addAll(jdkHomePaths);
+    return getBestJdkHomePath(list, langLevel);
+
+  }
+
+  @VisibleForTesting
+  @Nullable
+  static String getBestJdkHomePath(@NotNull Collection<String> jdkHomePaths, @NotNull LanguageLevel langLevel) {
+    // Search for JDKs in both the suggest folder and all its sub folders.
+    List<String> roots = Lists.newArrayList();
+    for (String jdkHomePath : jdkHomePaths) {
+      if (isNotEmpty(jdkHomePath)) {
+        roots.add(jdkHomePath);
+        roots.addAll(getChildrenPaths(jdkHomePath));
+      }
+    }
+    return getBestJdk(roots, langLevel);
+  }
+
+  @NotNull
+  private static List<String> getChildrenPaths(@NotNull String dirPath) {
+    File dir = new File(dirPath);
+    if (!dir.isDirectory()) {
+      return emptyList();
+    }
+    List<String> childrenPaths = Lists.newArrayList();
+    for (File child : notNullize(dir.listFiles())) {
+      boolean directory = child.isDirectory();
+      if (directory) {
+        childrenPaths.add(child.getAbsolutePath());
+      }
+    }
+    return childrenPaths;
+  }
+
+  @Nullable
+  private static String getBestJdk(@NotNull List<String> jdkRoots, @NotNull LanguageLevel langLevel) {
+    String bestJdk = null;
+    for (String jdkRoot : jdkRoots) {
+      if (JavaSdk.getInstance().isValidSdkHome(jdkRoot)) {
+        if (bestJdk == null && hasMatchingLangLevel(jdkRoot, langLevel)) {
+          bestJdk = jdkRoot;
+        }
+        else if (bestJdk != null) {
+          bestJdk = selectJdk(bestJdk, jdkRoot, langLevel);
+        }
+      }
+    }
+    return bestJdk;
+  }
+
+  @Nullable
+  private static String selectJdk(@NotNull String jdk1, @NotNull String jdk2, @NotNull LanguageLevel langLevel) {
+    if (hasMatchingLangLevel(jdk1, langLevel)) {
+      return jdk1;
+    }
+    if (hasMatchingLangLevel(jdk2, langLevel)) {
+      return jdk2;
+    }
+    return null;
+  }
+
+  private static boolean hasMatchingLangLevel(@NotNull String jdkRoot, @NotNull LanguageLevel langLevel) {
+    JavaSdkVersion version = getVersion(jdkRoot);
+    return hasMatchingLangLevel(version, langLevel);
+  }
+
+  @VisibleForTesting
+  static boolean hasMatchingLangLevel(@NotNull JavaSdkVersion jdkVersion, @NotNull LanguageLevel langLevel) {
+    LanguageLevel max = jdkVersion.getMaxLanguageLevel();
+    return max.isAtLeast(langLevel);
+  }
+
+  @NotNull
+  private static JavaSdkVersion getVersion(@NotNull String jdkRoot) {
+    String version = JavaSdk.getInstance().getVersionString(jdkRoot);
+    if (version == null) {
+      return JavaSdkVersion.JDK_1_0;
+    }
+    JavaSdkVersion sdkVersion = JavaSdk.getInstance().getVersion(version);
+    return sdkVersion == null ? JavaSdkVersion.JDK_1_0 : sdkVersion;
+  }
+
+  @Nullable
+  public static Sdk createJdk(@NotNull String jdkHomePath) {
+    Sdk jdk = createAndAddSDK(jdkHomePath, JavaSdk.getInstance());
+    if (jdk == null) {
+      String msg = String.format("Unable to create JDK from path '%1$s'", jdkHomePath);
+      Logger.getInstance(Jdks.class).error(msg);
+    }
+    return jdk;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/LibraryEditor.java b/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/LibraryEditor.java
new file mode 100644
index 0000000..a454628
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/LibraryEditor.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.projectstructure;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.java.libraries.SourceJarManager;
+import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.OrderRootType;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.openapi.roots.libraries.LibraryTable;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.util.io.FileUtilRt;
+import com.intellij.openapi.vfs.StandardFileSystems;
+import com.intellij.openapi.vfs.VirtualFileManager;
+import com.intellij.util.io.URLUtil;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Edits IntelliJ libraries
+ */
+public class LibraryEditor {
+  private static final Logger LOG = Logger.getInstance(LibraryEditor.class);
+
+  public static void updateProjectLibraries(Project project,
+                                            BlazeContext context,
+                                            BlazeProjectData blazeProjectData,
+                                            Collection<BlazeLibrary> newLibraries,
+                                            Collection<BlazeLibrary> oldLibraries) {
+    Set<LibraryKey> intelliJLibraryState = Sets.newHashSet();
+    for (Library library : ProjectLibraryTable.getInstance(project).getLibraries()) {
+      String name = library.getName();
+      if (name != null) {
+        intelliJLibraryState.add(LibraryKey.fromIntelliJLibraryName(name));
+      }
+    }
+    Collection<BlazeLibrary> librariesToUpdate = getUpdatedObjects(oldLibraries,
+                                                                   newLibraries,
+                                                                   intelliJLibraryState);
+    if (oldLibraries.isEmpty()) {
+      context.output(new PrintOutput(
+        String.format(
+          "Importing %d libraries",
+          librariesToUpdate.size())));
+    }
+    else {
+      String consoleMessage = String.format(
+        "Total libraries: %d\n"
+        + "Updating %d modified libraries",
+        newLibraries.size(),
+        librariesToUpdate.size());
+      context.output(new PrintOutput(consoleMessage));
+    }
+
+    Set<String> externallyAddedLibraries = Sets.newHashSet();
+    for (BlazeJavaSyncAugmenter syncAugmenter : BlazeJavaSyncAugmenter.EP_NAME.getExtensions()) {
+      externallyAddedLibraries.addAll(syncAugmenter.getExternallyAddedLibraries(blazeProjectData));
+    }
+
+    LibraryTable libraryTable = ProjectLibraryTable.getInstance(project);
+    LibraryTable.ModifiableModel libraryTableModel =
+      libraryTable.getModifiableModel();
+    try {
+      boolean attachSourcesByDefault = BlazeUserSettings.getInstance().getAttachSourcesByDefault();
+      SourceJarManager sourceJarManager = SourceJarManager.getInstance(project);
+      for (BlazeLibrary library : librariesToUpdate) {
+        boolean attachSources = attachSourcesByDefault || sourceJarManager.hasSourceJarAttached(library.getKey());
+        updateLibrary(libraryTable, libraryTableModel, library, attachSources);
+      }
+
+      // Garbage collect unused libraries
+      Set<LibraryKey> newLibraryKeys = newLibraries.stream().map(BlazeLibrary::getKey).collect(Collectors.toSet());
+      for (LibraryKey libraryKey : intelliJLibraryState) {
+        String libraryIntellijName = libraryKey.getIntelliJLibraryName();
+        if (!newLibraryKeys.contains(libraryKey) && !externallyAddedLibraries.contains(libraryIntellijName)) {
+          Library library = libraryTable.getLibraryByName(libraryIntellijName);
+          if (library != null) {
+            libraryTableModel.removeLibrary(library);
+          }
+        }
+      }
+    }
+    finally {
+      libraryTableModel.commit();
+    }
+  }
+
+
+  public static void updateLibrary(
+    LibraryTable libraryTable,
+    LibraryTable.ModifiableModel libraryTableModel,
+    BlazeLibrary blazeLibrary,
+    boolean attachSourceJar) {
+    String libraryName = blazeLibrary.getKey().getIntelliJLibraryName();
+    Library library = libraryTable.getLibraryByName(libraryName);
+    if (library != null) {
+      libraryTableModel.removeLibrary(library);
+    }
+    library = libraryTableModel.createLibrary(libraryName);
+
+    Library.ModifiableModel libraryModel = library.getModifiableModel();
+    try {
+      LibraryArtifact libraryArtifact = blazeLibrary.getLibraryArtifact();
+      if (libraryArtifact != null) {
+        libraryModel.addRoot(
+          pathToUrl(libraryArtifact.jar.getFile()),
+          OrderRootType.CLASSES
+        );
+        if (attachSourceJar && libraryArtifact.sourceJar != null) {
+          libraryModel.addRoot(
+            pathToUrl(libraryArtifact.sourceJar.getFile()),
+            OrderRootType.SOURCES
+          );
+        }
+      }
+      if (blazeLibrary.getSources() != null) {
+        for (File file : blazeLibrary.getSources()) {
+          libraryModel.addRoot(
+            pathToUrl(file),
+            OrderRootType.SOURCES
+          );
+        }
+      }
+    }
+    finally {
+      libraryModel.commit();
+    }
+  }
+
+  static Collection<BlazeLibrary> getUpdatedObjects(Collection<BlazeLibrary> oldObjects,
+                                                    Collection<BlazeLibrary> newObjects,
+                                                    Set<LibraryKey> intelliJState) {
+    List<BlazeLibrary> result = Lists.newArrayList();
+    Set<BlazeLibrary> oldObjectSet = Sets.newHashSet(oldObjects);
+    for (BlazeLibrary value : newObjects) {
+      LibraryKey key = value.getKey();
+      if (!intelliJState.contains(key) || !oldObjectSet.contains(value)) {
+        result.add(value);
+      }
+    }
+    return result;
+  }
+
+  private static String pathToUrl(File path) {
+    String name = path.getName();
+    boolean isJarFile = 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;
+    String filePath = FileUtil.toSystemIndependentName(path.getPath());
+    String url = VirtualFileManager.constructUrl(protocol, filePath);
+    if (isJarFile) {
+      url += URLUtil.JAR_SEPARATOR;
+    }
+    return url;
+  }
+
+  public static void configureDependencies(
+    Project project,
+    BlazeContext context,
+    ModifiableRootModel modifiableRootModel,
+    Collection<BlazeLibrary> libraries) {
+    for (BlazeLibrary library : libraries) {
+      updateLibraryDependency(modifiableRootModel, library.getKey());
+    }
+  }
+
+  private static void updateLibraryDependency(
+    ModifiableRootModel model,
+    LibraryKey libraryKey) {
+    LibraryTable libraryTable = ProjectLibraryTable.getInstance(model.getProject());
+    Library library = libraryTable.getLibraryByName(libraryKey.getIntelliJLibraryName());
+    if (library == null) {
+      LOG.error("Library missing: " + libraryKey.getIntelliJLibraryName() + ". Please resync project to resolve.");
+      return;
+    }
+    model.addLibraryEntry(library);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/SourceFolderEditor.java b/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/SourceFolderEditor.java
new file mode 100644
index 0000000..a24d559
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/projectstructure/SourceFolderEditor.java
@@ -0,0 +1,118 @@
+/*
+ * 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 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;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Map;
+
+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/blaze-java/src/com/google/idea/blaze/java/sync/source/FilePathJavaPackageReader.java b/blaze-java/src/com/google/idea/blaze/java/sync/source/FilePathJavaPackageReader.java
new file mode 100644
index 0000000..ba9f1f7
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/source/FilePathJavaPackageReader.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.java.sync.source;
+
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.util.PackagePrefixCalculator;
+
+/**
+ * Gets the package from a java file by its file path alone (i.e. without opening the file).
+ */
+public final class FilePathJavaPackageReader extends JavaPackageReader {
+  @Override
+  public String getDeclaredPackageOfJavaFile(BlazeContext context, SourceArtifact sourceArtifact) {
+    String directory = sourceArtifact.artifactLocation.getRelativePath();
+    int i = directory.lastIndexOf('/');
+    if (i >= 0) {
+      directory = directory.substring(0, i);
+    }
+    return PackagePrefixCalculator.packagePrefixOf(new WorkspacePath(directory));
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/source/JavaPackageReader.java b/blaze-java/src/com/google/idea/blaze/java/sync/source/JavaPackageReader.java
new file mode 100644
index 0000000..5cf63c3
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/source/JavaPackageReader.java
@@ -0,0 +1,28 @@
+/*
+ * 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.source;
+
+import com.google.idea.blaze.base.scope.BlazeContext;
+
+import javax.annotation.Nullable;
+
+/**
+ * Reads java packages from files.
+ */
+public abstract class JavaPackageReader {
+  @Nullable
+  abstract String getDeclaredPackageOfJavaFile(BlazeContext context, SourceArtifact sourceArtifact);
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java b/blaze-java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java
new file mode 100644
index 0000000..a9001f2
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/source/JavaSourcePackageReader.java
@@ -0,0 +1,79 @@
+/*
+ * 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.source;
+
+import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.diagnostic.Logger;
+
+import javax.annotation.Nullable;
+import java.io.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parse package string directly from java source
+ */
+public class JavaSourcePackageReader extends JavaPackageReader {
+
+  public static JavaSourcePackageReader getInstance() {
+    return ServiceManager.getService(JavaSourcePackageReader.class);
+  }
+
+  private static final Logger LOG = Logger.getInstance(SourceDirectoryCalculator.class);
+
+  private static final Pattern JAVA_PACKAGE_PATTERN =
+    Pattern.compile("^\\s*package\\s+([\\w\\.]+);");
+
+  @Override
+  @Nullable
+  public String getDeclaredPackageOfJavaFile(BlazeContext context, SourceArtifact sourceArtifact) {
+    if (sourceArtifact.artifactLocation.isGenerated()) {
+      return null;
+    }
+    InputStreamProvider inputStreamProvider = InputStreamProvider.getInstance();
+    File sourceFile = sourceArtifact.artifactLocation.getFile();
+    try (InputStream javaInputStream = inputStreamProvider.getFile(sourceFile)) {
+      BufferedReader javaReader = new BufferedReader(new InputStreamReader(javaInputStream));
+      String javaLine;
+
+      while ((javaLine = javaReader.readLine()) != null) {
+        Matcher packageMatch = JAVA_PACKAGE_PATTERN.matcher(javaLine);
+        if (packageMatch.find()) {
+          return packageMatch.group(1);
+        }
+      }
+      IssueOutput
+        .warn("No package name string found in java source file: " + sourceFile)
+        .inFile(sourceFile)
+        .submit(context);
+      return null;
+    }
+    catch (FileNotFoundException e) {
+      IssueOutput
+        .warn("No source file found for: " + sourceFile)
+        .inFile(sourceFile)
+        .submit(context);
+      return null;
+    }
+    catch (IOException e) {
+      LOG.error(e);
+      return null;
+    }
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/source/ManifestFilePackageReader.java b/blaze-java/src/com/google/idea/blaze/java/sync/source/ManifestFilePackageReader.java
new file mode 100644
index 0000000..71b1707
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/source/ManifestFilePackageReader.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.source;
+
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.scope.BlazeContext;
+
+import javax.annotation.Nullable;
+import java.util.Map;
+
+public class ManifestFilePackageReader extends JavaPackageReader {
+
+  private final Map<Label, Map<String, String>> manifestMap;
+
+  public ManifestFilePackageReader(Map<Label, Map<String, String>> manifestMap) {
+    this.manifestMap = manifestMap;
+  }
+
+  @Nullable
+  @Override
+  String getDeclaredPackageOfJavaFile(BlazeContext context, SourceArtifact sourceArtifact) {
+    Map<String, String> manifestMapForRule = manifestMap.get(sourceArtifact.originatingRule);
+    if (manifestMapForRule != null) {
+      return manifestMapForRule.get(sourceArtifact.artifactLocation.getFile().getPath());
+    }
+    return null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java b/blaze-java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
new file mode 100644
index 0000000..afa6ba7
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/source/PackageManifestReader.java
@@ -0,0 +1,151 @@
+/*
+ * 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.source;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.idea.blaze.base.async.FutureUtil;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.prefetch.PrefetchService;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.filediff.FileDiffService;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.JavaSourcePackage;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.PackageManifest;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.diagnostic.Logger;
+
+import javax.annotation.Nullable;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+
+public class PackageManifestReader {
+  private static final Logger LOG = Logger.getInstance(SourceDirectoryCalculator.class);
+
+  public static PackageManifestReader getInstance() {
+    return ServiceManager.getService(PackageManifestReader.class);
+  }
+
+  private FileDiffService fileDiffService = new FileDiffService();
+  private FileDiffService.State fileDiffState;
+
+  private Map<File, Label> fileToLabelMap = Maps.newHashMap();
+  private final Map<Label, Map<String, String>> manifestMap = Maps.newConcurrentMap();
+
+  /**
+   * @return A map from java source absolute file path to declared package string.
+   */
+  public Map<Label, Map<String, String>> readPackageManifestFiles(
+    BlazeContext context,
+    ArtifactLocationDecoder decoder,
+    Map<Label, ArtifactLocation> javaPackageManifests,
+    ListeningExecutorService executorService) {
+
+    Map<File, Label> fileToLabelMap = Maps.newHashMap();
+    for (Map.Entry<Label, ArtifactLocation> entry : javaPackageManifests.entrySet()) {
+      Label label = entry.getKey();
+      File file = entry.getValue().getFile();
+      fileToLabelMap.put(file, label);
+    }
+    List<File> updatedFiles = Lists.newArrayList();
+    List<File> removedFiles = Lists.newArrayList();
+    fileDiffState = fileDiffService.updateFiles(fileDiffState, fileToLabelMap.keySet(), updatedFiles, removedFiles);
+
+    ListenableFuture<?> fetchFuture = PrefetchService.getInstance().prefetchFiles(updatedFiles, true);
+    if (!FutureUtil.waitForFuture(context, fetchFuture)
+      .timed("FetchPackageManifests")
+      .run()
+      .success()) {
+      return null;
+    }
+
+    List<ListenableFuture<Void>> futures = Lists.newArrayList();
+    for (File file : updatedFiles) {
+      futures.add(executorService.submit(() -> {
+        Map<String, String> manifest = parseManifestFile(decoder, file);
+        manifestMap.put(fileToLabelMap.get(file), manifest);
+        return null;
+      }));
+    }
+    for (File file : removedFiles) {
+      Label label = this.fileToLabelMap.get(file);
+      if (label != null) {
+        manifestMap.remove(label);
+      }
+    }
+    this.fileToLabelMap = fileToLabelMap;
+
+    try {
+      Futures.allAsList(futures).get();
+    } catch (ExecutionException | InterruptedException e) {
+      LOG.error(e);
+      throw new IllegalStateException("Could not read sources");
+    }
+    return manifestMap;
+  }
+
+  protected Map<String, String> parseManifestFile(ArtifactLocationDecoder decoder,
+                                                  File packageManifest) {
+    Map<String, String> outputMap = Maps.newHashMap();
+    InputStreamProvider inputStreamProvider = InputStreamProvider.getInstance();
+
+    try (InputStream input = inputStreamProvider.getFile(packageManifest)) {
+      try (BufferedInputStream bufferedInputStream = new BufferedInputStream(input)) {
+        PackageManifest proto = PackageManifest.parseFrom(bufferedInputStream);
+        for (JavaSourcePackage source : proto.getSourcesList()) {
+          String absPath = getAbsolutePath(decoder, source);
+          if (absPath != null) {
+            outputMap.put(absPath, source.getPackageString());
+          }
+        }
+      }
+      return outputMap;
+    }
+    catch (IOException e) {
+      LOG.error(e);
+      return outputMap;
+    }
+  }
+
+  /**
+   * Returns null if the artifact location file can't be found,
+   * presumably because it's been removed from the file system since the blaze build.
+   */
+  @Nullable
+  private static String getAbsolutePath(ArtifactLocationDecoder decoder,
+                                        JavaSourcePackage source) {
+    if (!source.hasArtifactLocation()) {
+      return source.getAbsolutePath();
+    }
+    ArtifactLocation location = decoder.decode(source.getArtifactLocation());
+    if (location == null) {
+      return null;
+    }
+    return location.getFile().getAbsolutePath();
+  }
+
+}
+
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/source/SourceArtifact.java b/blaze-java/src/com/google/idea/blaze/java/sync/source/SourceArtifact.java
new file mode 100644
index 0000000..4f9c877
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/source/SourceArtifact.java
@@ -0,0 +1,58 @@
+/*
+ * 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.source;
+
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.model.primitives.Label;
+
+/**
+ * Pairing of rule and source artifact.
+ */
+public class SourceArtifact {
+  public final Label originatingRule;
+  public final ArtifactLocation artifactLocation;
+
+  public SourceArtifact(Label originatingRule, ArtifactLocation artifactLocation) {
+    this.originatingRule = originatingRule;
+    this.artifactLocation = artifactLocation;
+  }
+
+  public static Builder builder(Label originatingRule) {
+    return new Builder(originatingRule);
+  }
+
+  public static class Builder {
+    Label originatingRule;
+    ArtifactLocation artifactLocation;
+
+    Builder(Label originatingRule) {
+      this.originatingRule = originatingRule;
+    }
+
+    public Builder setArtifactLocation(ArtifactLocation artifactLocation) {
+      this.artifactLocation = artifactLocation;
+      return this;
+    }
+
+    public Builder setArtifactLocation(ArtifactLocation.Builder artifactLocation) {
+      return setArtifactLocation(artifactLocation.build());
+    }
+
+    public SourceArtifact build() {
+      return new SourceArtifact(originatingRule, artifactLocation);
+    }
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java b/blaze-java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
new file mode 100644
index 0000000..c312bbe
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculator.java
@@ -0,0 +1,468 @@
+/*
+ * 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.source;
+
+import com.google.common.base.*;
+import com.google.common.base.Objects;
+import com.google.common.collect.*;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.idea.blaze.base.async.executor.TransientExecutor;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.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;
+import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import com.intellij.openapi.diagnostic.Logger;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.*;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+/**
+ * This is a utility class for calculating the java sources and their package prefixes given
+ * a module and its Blaze {@link ArtifactLocation} list.
+ */
+public final class SourceDirectoryCalculator {
+
+  private static final Logger LOG = Logger.getInstance(SourceDirectoryCalculator.class);
+
+  private static final Splitter PACKAGE_SPLITTER = Splitter.on('.');
+  private static final Splitter PATH_SPLITTER = Splitter.on('/');
+  private static final Joiner PACKAGE_JOINER = Joiner.on('.');
+  private static final Joiner PATH_JOINER = Joiner.on('/');
+
+  private static final Comparator<WorkspacePath> WORKSPACE_PATH_COMPARATOR =
+    (o1, o2) -> o1.relativePath().compareTo(o2.relativePath());
+
+  private static final JavaPackageReader generatedFileJavaPackageReader = new FilePathJavaPackageReader();
+  private final ListeningExecutorService executorService = MoreExecutors.sameThreadExecutor();
+  private final ListeningExecutorService packageReaderExecutorService = MoreExecutors.listeningDecorator(new TransientExecutor(16));
+
+  public ImmutableList<BlazeContentEntry> calculateContentEntries(
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    SourceTestConfig sourceTestConfig,
+    ArtifactLocationDecoder artifactLocationDecoder,
+    Collection<WorkspacePath> rootDirectories,
+    Collection<SourceArtifact> sources,
+    Map<Label, ArtifactLocation> javaPackageManifests) {
+
+    ManifestFilePackageReader manifestFilePackageReader = Scope.push(context, (childContext) -> {
+      childContext.push(new TimingScope("ReadPackageManifests"));
+      Map<Label, Map<String, String>> manifestMap = PackageManifestReader.getInstance().readPackageManifestFiles(
+        childContext,
+        artifactLocationDecoder,
+        javaPackageManifests,
+        packageReaderExecutorService
+      );
+      return new ManifestFilePackageReader(manifestMap);
+    });
+
+    final List<JavaPackageReader> javaPackageReaders = Lists.newArrayList(
+      manifestFilePackageReader,
+      JavaSourcePackageReader.getInstance(),
+      generatedFileJavaPackageReader);
+
+    Collection<SourceArtifact> nonGeneratedSources = filterGeneratedArtifacts(sources);
+
+    // Sort artifacts and excludes into their respective workspace paths
+    Multimap<WorkspacePath, SourceArtifact> sourcesUnderDirectoryRoot =
+      sortArtifactLocationsByRootDirectory(context, rootDirectories, nonGeneratedSources);
+
+    List<BlazeContentEntry> result = Lists.newArrayList();
+    for (WorkspacePath workspacePath : rootDirectories) {
+      File contentRoot = workspaceRoot.fileForPath(workspacePath);
+      ImmutableList<BlazeSourceDirectory> sourceDirectories = calculateSourceDirectoriesForContentRoot(
+        context,
+        sourceTestConfig,
+        workspaceRoot,
+        workspacePath,
+        sourcesUnderDirectoryRoot.get(workspacePath),
+        javaPackageReaders
+      );
+      if (!sourceDirectories.isEmpty()) {
+        result.add(new BlazeContentEntry(contentRoot, sourceDirectories));
+      }
+    }
+    Collections.sort(result, (lhs, rhs) -> lhs.contentRoot.compareTo(rhs.contentRoot));
+    return ImmutableList.copyOf(result);
+  }
+
+  private Collection<SourceArtifact> filterGeneratedArtifacts(Collection<SourceArtifact> artifactLocations) {
+    return artifactLocations
+      .stream()
+      .filter(sourceArtifact -> sourceArtifact.artifactLocation.isSource())
+      .collect(Collectors.toList());
+  }
+
+  private static Multimap<WorkspacePath, SourceArtifact> sortArtifactLocationsByRootDirectory(
+    BlazeContext context,
+    Collection<WorkspacePath> rootDirectories,
+    Collection<SourceArtifact> sources) {
+
+    Multimap<WorkspacePath, SourceArtifact> result = ArrayListMultimap.create();
+
+    for (SourceArtifact sourceArtifact : sources) {
+      WorkspacePath foundWorkspacePath = rootDirectories
+        .stream()
+        .filter(rootDirectory -> isUnderRootDirectory(rootDirectory, sourceArtifact.artifactLocation.getRelativePath()))
+        .findFirst()
+        .orElse(null);
+
+      if (foundWorkspacePath != null) {
+        result.put(foundWorkspacePath, sourceArtifact);
+      }
+      else if (sourceArtifact.artifactLocation.isSource()) {
+        File sourceFile = sourceArtifact.artifactLocation.getFile();
+        String message = String.format(
+          "Did not add %s. You're probably using a java file from outside the workspace"
+          + "that has been exported using export_files. Don't do that.", sourceFile);
+        IssueOutput
+          .warn(message)
+          .inFile(sourceFile)
+          .submit(context);
+      }
+    }
+    return result;
+  }
+
+  private static boolean isUnderRootDirectory(WorkspacePath rootDirectory, String relativePath) {
+    if (rootDirectory.isWorkspaceRoot()) {
+      return true;
+    }
+    String rootDirectoryString = rootDirectory.toString();
+    return relativePath.startsWith(rootDirectoryString)
+      && (relativePath.length() == rootDirectoryString.length()
+          || (relativePath.charAt(rootDirectoryString.length()) == '/'));
+  }
+
+  /**
+   * Calculates all source directories for a single content root.
+   */
+  private ImmutableList<BlazeSourceDirectory> calculateSourceDirectoriesForContentRoot(
+    BlazeContext context,
+    SourceTestConfig sourceTestConfig,
+    WorkspaceRoot workspaceRoot,
+    WorkspacePath directoryRoot,
+    Collection<SourceArtifact> sourceArtifacts,
+    Collection<JavaPackageReader> javaPackageReaders) {
+
+    // Split out java files
+    List<SourceArtifact> javaArtifacts = Lists.newArrayList();
+    for (SourceArtifact sourceArtifact : sourceArtifacts) {
+      if (isJavaFile(sourceArtifact.artifactLocation)) {
+        javaArtifacts.add(sourceArtifact);
+      }
+    }
+
+    List<BlazeSourceDirectory> result = Lists.newArrayList();
+
+    // Add java source directories
+    calculateJavaSourceDirectories(
+      context,
+      workspaceRoot,
+      directoryRoot,
+      sourceTestConfig,
+      javaArtifacts,
+      javaPackageReaders,
+      result
+    );
+
+    Collections.sort(result, BlazeSourceDirectory.COMPARATOR);
+    return ImmutableList.copyOf(result);
+  }
+
+  /**
+   * Adds the java source directories.
+   */
+  private void calculateJavaSourceDirectories(
+    BlazeContext context,
+    WorkspaceRoot workspaceRoot,
+    WorkspacePath directoryRoot,
+    SourceTestConfig sourceTestConfig,
+    Collection<SourceArtifact> javaArtifacts,
+    Collection<JavaPackageReader> javaPackageReaders,
+    Collection<BlazeSourceDirectory> result) {
+
+    List<SourceRoot> sourceRootsPerFile = Lists.newArrayList();
+
+    // Get java sources
+    List<ListenableFuture<SourceRoot>> sourceRootFutures = Lists.newArrayList();
+    for (final SourceArtifact sourceArtifact : javaArtifacts) {
+      ListenableFuture<SourceRoot> future = executorService.submit(() -> sourceRootForJavaSource(
+        context,
+        sourceArtifact,
+        javaPackageReaders
+      ));
+      sourceRootFutures.add(future);
+    }
+    try {
+      for (SourceRoot sourceRoot : Futures.allAsList(sourceRootFutures).get()) {
+        if (sourceRoot != null) {
+          sourceRootsPerFile.add(sourceRoot);
+        }
+      }
+    }
+    catch (ExecutionException | InterruptedException e) {
+      LOG.error(e);
+      throw new IllegalStateException("Could not read sources");
+    }
+
+    // Sort source roots into their respective directories
+    Multimap<WorkspacePath, SourceRoot> sourceDirectoryToSourceRoots = HashMultimap.create();
+    for (SourceRoot sourceRoot : sourceRootsPerFile) {
+      sourceDirectoryToSourceRoots.put(sourceRoot.workspacePath, sourceRoot);
+    }
+
+    // Create a mapping from directory to package prefix
+    Map<WorkspacePath, SourceRoot> workspacePathToCandidateRoot = Maps.newHashMap();
+    for (WorkspacePath workspacePath : sourceDirectoryToSourceRoots.keySet()) {
+      Collection<SourceRoot> sources = sourceDirectoryToSourceRoots.get(workspacePath);
+      Multiset<String> packages = HashMultiset.create();
+
+      for (SourceRoot source : sources) {
+        packages.add(source.packagePrefix);
+      }
+
+      final String directoryPackagePrefix;
+      // Common case -- all source files agree on a single package
+      if (packages.elementSet().size() == 1) {
+        directoryPackagePrefix = packages.elementSet().iterator().next();
+      }
+      else {
+        directoryPackagePrefix = pickMostFrequentlyOccurring(packages);
+      }
+
+      // These properties must be the same for all files in the directory
+      SourceRoot sourceFile = sources.iterator().next();
+
+      SourceRoot candidateRoot = new SourceRoot(workspacePath, directoryPackagePrefix);
+      workspacePathToCandidateRoot.put(workspacePath, candidateRoot);
+    }
+
+    // Add content entry base if it doesn't exist
+    if (!workspacePathToCandidateRoot.containsKey(directoryRoot)) {
+      SourceRoot candidateRoot = new SourceRoot(directoryRoot, PackagePrefixCalculator.packagePrefixOf(directoryRoot));
+      workspacePathToCandidateRoot.put(directoryRoot, candidateRoot);
+    }
+
+    // Merge source roots
+    // We have to do this in directory order to ensure we encounter roots before
+    // their subdirectories
+    Map<WorkspacePath, SourceRoot> mergedSourceRoots = Maps.newHashMap();
+    List<WorkspacePath> sortedWorkspacePaths = Lists.newArrayList(workspacePathToCandidateRoot.keySet());
+    Collections.sort(sortedWorkspacePaths, WORKSPACE_PATH_COMPARATOR);
+    for (WorkspacePath workspacePath : sortedWorkspacePaths) {
+      SourceRoot candidateRoot = workspacePathToCandidateRoot.get(workspacePath);
+      SourceRoot bestNewRoot = candidateRoot;
+      for (SourceRoot mergedSourceRoot : new CandidateRoots(directoryRoot, candidateRoot)) {
+        SourceRoot existingSourceRoot = mergedSourceRoots.get(mergedSourceRoot.workspacePath);
+        if (existingSourceRoot != null) {
+          if (existingSourceRoot.packagePrefix.equals(mergedSourceRoot.packagePrefix)) {
+            // Do not create new source root -- merge into preexisting source root
+            // Since we already decided to establish one here, there is also
+            // no need to go further up the tree
+            bestNewRoot = null;
+          }
+          break;
+        }
+        bestNewRoot = mergedSourceRoot;
+      }
+
+      if (bestNewRoot != null) {
+        mergedSourceRoots.put(bestNewRoot.workspacePath, bestNewRoot);
+      }
+    }
+
+    // Add merged source roots
+    for (SourceRoot sourceRoot : mergedSourceRoots.values()) {
+      result.add(BlazeSourceDirectory.builder(workspaceRoot.fileForPath(sourceRoot.workspacePath))
+                              .setPackagePrefix(sourceRoot.packagePrefix)
+                              .setTest(sourceTestConfig.isTestSource(sourceRoot.workspacePath.relativePath()))
+                              .setGenerated(false)
+                              .build());
+    }
+  }
+
+  private static <T> T pickMostFrequentlyOccurring(Multiset<T> set) {
+    Preconditions.checkArgument(set.size() > 0);
+
+    T best = null;
+    int bestCount = 0;
+
+    for (T candidate : set.elementSet()) {
+      int candidateCount = set.count(candidate);
+      if (candidateCount > bestCount) {
+        best = candidate;
+        bestCount = candidateCount;
+      }
+    }
+    return best;
+  }
+
+  @Nullable
+  private static SourceRoot sourceRootForJavaSource(
+    BlazeContext context,
+    SourceArtifact sourceArtifact,
+    Collection<JavaPackageReader> javaPackageReaders) {
+
+    File javaFile = sourceArtifact.artifactLocation.getFile();
+
+    String declaredPackage = null;
+    for (JavaPackageReader reader : javaPackageReaders) {
+      declaredPackage = reader.getDeclaredPackageOfJavaFile(context, sourceArtifact);
+      if (declaredPackage != null) {
+        break;
+      }
+    }
+    if (declaredPackage == null) {
+      IssueOutput
+        .warn("Failed to inspect the package name of java source file: " + javaFile)
+        .inFile(javaFile)
+        .submit(context);
+      return null;
+    }
+    return new SourceRoot(
+      new WorkspacePath(new File(sourceArtifact.artifactLocation.getRelativePath()).getParent()),
+      declaredPackage
+    );
+  }
+
+  static class SourceRoot {
+    final WorkspacePath workspacePath;
+    final String packagePrefix;
+    public SourceRoot(WorkspacePath workspacePath, String packagePrefix) {
+      this.workspacePath = workspacePath;
+      this.packagePrefix = packagePrefix;
+    }
+    @Override
+    public boolean equals(Object o) {
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      SourceRoot that = (SourceRoot)o;
+      return Objects.equal(workspacePath, that.workspacePath)
+             && Objects.equal(packagePrefix, that.packagePrefix);
+    }
+    @Override
+    public int hashCode() {
+      return Objects.hashCode(workspacePath, packagePrefix);
+    }
+
+    @Override
+    public String toString() {
+      return "SourceRoot {" + '\n'
+             + "  workspacePath: " + workspacePath + '\n'
+             + "  packagePrefix: " + packagePrefix + '\n'
+             + '}';
+    }
+  }
+
+  private static boolean isJavaFile(ArtifactLocation artifactLocation) {
+    return artifactLocation.getRelativePath().endsWith(".java");
+  }
+
+  static class CandidateRoots implements Iterable<SourceRoot> {
+    private static final List<String> EMPTY_LIST = ImmutableList.of();
+
+    private final SourceRoot candidateRoot;
+    private final WorkspacePath directoryRoot;
+
+    CandidateRoots(
+      WorkspacePath directoryRoot,
+      SourceRoot candidateRoot) {
+      this.directoryRoot = directoryRoot;
+      this.candidateRoot = candidateRoot;
+    }
+
+    @Override
+    public Iterator<SourceRoot> iterator() {
+      return new CandidateRootIterator();
+    }
+
+    class CandidateRootIterator implements Iterator<SourceRoot> {
+      private final List<String> packageComponents;
+      private final List<String> pathComponents;
+      private int packageIndex;
+      private int pathIndex;
+
+      CandidateRootIterator() {
+        int directoryRootLength = directoryRoot.relativePath().length();
+        String relativePath = candidateRoot.workspacePath.relativePath();
+        final String sourcePathRelativeToModule;
+        if (relativePath.length() > directoryRootLength) {
+          if (directoryRootLength > 0) {
+            sourcePathRelativeToModule = relativePath.substring(directoryRootLength + 1);
+          } else {
+            sourcePathRelativeToModule = relativePath;
+          }
+        } else {
+          sourcePathRelativeToModule = "";
+        }
+
+        this.packageComponents = PACKAGE_SPLITTER.splitToList(candidateRoot.packagePrefix);
+        this.pathComponents = !Strings.isNullOrEmpty(sourcePathRelativeToModule)
+                              ? PATH_SPLITTER.splitToList(sourcePathRelativeToModule) : EMPTY_LIST;
+        this.packageIndex = packageComponents.size() - 1;
+        this.pathIndex = pathComponents.size() - 1;
+      }
+
+      @Override
+      public boolean hasNext() {
+        return (packageIndex >= 0 && pathIndex >= 0 && packageComponents.get(packageIndex).equals(pathComponents.get(pathIndex)));
+      }
+
+      @Override
+      public SourceRoot next() {
+        String directoryRootRelativePath = PATH_JOINER.join(pathComponents.subList(0, pathIndex));
+        final WorkspacePath workspacePath;
+        if (directoryRootRelativePath.isEmpty()){
+          workspacePath = directoryRoot;
+        } else if (directoryRoot.isWorkspaceRoot()) {
+          workspacePath = new WorkspacePath(directoryRootRelativePath);
+        } else {
+          workspacePath = new WorkspacePath(PATH_JOINER.join(directoryRoot.relativePath(), directoryRootRelativePath));
+        }
+
+        SourceRoot sourceRoot = new SourceRoot(
+          workspacePath,
+          PACKAGE_JOINER.join(packageComponents.subList(0, packageIndex))
+        );
+        --packageIndex;
+        --pathIndex;
+        return sourceRoot;
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    }
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/sync/workingset/JavaWorkingSet.java b/blaze-java/src/com/google/idea/blaze/java/sync/workingset/JavaWorkingSet.java
new file mode 100644
index 0000000..f804eb3
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/sync/workingset/JavaWorkingSet.java
@@ -0,0 +1,88 @@
+/*
+ * 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.workingset;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+
+import java.util.Set;
+
+/**
+ * Computes the working set of files of directories from source control.
+ *
+ * The working set is:
+ *   - All new untracked directories (git only)
+ *   - All modified BUILD files
+ *   - All modified java files
+ *
+ * A rule is considered part of the working set if any of the following is true:
+ *   - Its BUILD file is modified
+ *   - Its BUILD file is under a new directory
+ *   - Any of its java files are modified
+ *   - Any of its java files are under a new directory
+ *
+ * Rules in the working set get an expanded classpath of their direct deps,
+ * i.e. they temporarily defeat classpath reduction.
+ */
+public class JavaWorkingSet {
+  private final Set<String> modifiedBuildFileRelativePaths;
+  private final Set<String> modifiedJavaFileRelativePaths;
+
+  public JavaWorkingSet(WorkspaceRoot workspaceRoot, WorkingSet workingSet) {
+    Set<String> modifiedBuildFileRelativePaths = Sets.newHashSet();
+    Set<String> modifiedJavaFileRelativePaths = Sets.newHashSet();
+
+    for (WorkspacePath workspacePath : Iterables.concat(workingSet.addedFiles, workingSet.modifiedFiles)) {
+      if (workspaceRoot.fileForPath(workspacePath).getName().equals("BUILD")) {
+        modifiedBuildFileRelativePaths.add(workspacePath.relativePath());
+      } else if (workspacePath.relativePath().endsWith(".java")){
+        modifiedJavaFileRelativePaths.add(workspacePath.relativePath());
+      }
+    }
+
+    this.modifiedBuildFileRelativePaths = modifiedBuildFileRelativePaths;
+    this.modifiedJavaFileRelativePaths = modifiedJavaFileRelativePaths;
+  }
+
+  public boolean isRuleInWorkingSet(RuleIdeInfo ruleIdeInfo) {
+    ArtifactLocation buildFile = ruleIdeInfo.buildFile;
+    if (buildFile != null) {
+      if (modifiedBuildFileRelativePaths.contains(buildFile.getRelativePath())) {
+        return true;
+      }
+    }
+
+    for (ArtifactLocation artifactLocation : ruleIdeInfo.sources) {
+      if (isInWorkingSet(artifactLocation)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public boolean isInWorkingSet(ArtifactLocation artifactLocation) {
+    return isInWorkingSet(artifactLocation.getRelativePath());
+  }
+
+  boolean isInWorkingSet(String relativePath) {
+    return modifiedJavaFileRelativePaths.contains(relativePath);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java b/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java
new file mode 100644
index 0000000..dd61284
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusClassNodeDecorator.java
@@ -0,0 +1,63 @@
+/*
+ * 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.syncstatus;
+
+import com.intellij.ide.projectView.PresentationData;
+import com.intellij.ide.projectView.ProjectViewNode;
+import com.intellij.ide.projectView.ProjectViewNodeDecorator;
+import com.intellij.ide.projectView.impl.nodes.ClassTreeNode;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.packageDependencies.ui.PackageDependenciesNode;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiFile;
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.SimpleTextAttributes;
+
+/**
+ * Grays out any unreachable java classes.
+ */
+public class BlazeJavaSyncStatusClassNodeDecorator implements ProjectViewNodeDecorator {
+  @Override
+  public void decorate(ProjectViewNode node, PresentationData data) {
+    if (!(node instanceof ClassTreeNode)) {
+      return;
+    }
+    PsiClass psiClass = ((ClassTreeNode)node).getPsiClass();
+    if (psiClass == null) {
+      return;
+    }
+    PsiFile psiFile = psiClass.getContainingFile();
+    if (psiFile == null) {
+      return;
+    }
+    VirtualFile virtualFile = psiFile.getVirtualFile();
+    if (virtualFile == null) {
+      return;
+    }
+
+    Project project = node.getProject();
+    if (SyncStatusHelper.isUnsynced(project, virtualFile)) {
+      data.clearText();
+      data.addText(psiClass.getName(), SimpleTextAttributes.GRAY_ATTRIBUTES);
+      data.addText(" (unsynced)", SimpleTextAttributes.GRAY_ATTRIBUTES);
+    }
+  }
+
+  @Override
+  public void decorate(PackageDependenciesNode node, ColoredTreeCellRenderer cellRenderer) {
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java b/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
new file mode 100644
index 0000000..0c9e188
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabColorProvider.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.syncstatus;
+
+import com.intellij.openapi.fileEditor.impl.EditorTabColorProvider;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.ui.JBColor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.*;
+
+/**
+ * Changes the color for unsynced files.
+ */
+public class BlazeJavaSyncStatusEditorTabColorProvider implements EditorTabColorProvider {
+  private static final JBColor UNSYNCED_COLOR = new JBColor(new Color(252, 234, 234), new Color(121, 105, 105));
+
+  @Nullable
+  @Override
+  public Color getEditorTabColor(@NotNull Project project, @NotNull VirtualFile file) {
+    if (file.getName().endsWith(".java") && SyncStatusHelper.isUnsynced(project, file)) {
+      return UNSYNCED_COLOR;
+    }
+    return null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java b/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.java
new file mode 100644
index 0000000..642ed83
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/syncstatus/BlazeJavaSyncStatusEditorTabTitleProvider.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.java.syncstatus;
+
+import com.intellij.openapi.fileEditor.impl.EditorTabTitleProvider;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Changes the tab title for unsynced files.
+ */
+public class BlazeJavaSyncStatusEditorTabTitleProvider implements EditorTabTitleProvider {
+  @Nullable
+  @Override
+  public String getEditorTabTitle(Project project, VirtualFile file) {
+    if (file.getName().endsWith("java") && SyncStatusHelper.isUnsynced(project, file)) {
+      return file.getPresentableName() + " (unsynced)";
+    }
+    return null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java b/blaze-java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
new file mode 100644
index 0000000..d2a3cf9
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/syncstatus/SyncStatusHelper.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.syncstatus;
+
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import java.io.File;
+
+class SyncStatusHelper {
+  static boolean isUnsynced(Project project, VirtualFile virtualFile) {
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(project).getBlazeProjectData();
+    if (blazeProjectData == null) {
+      return false;
+    }
+    BlazeJavaSyncData syncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    if (syncData == null) {
+      return false;
+    }
+    if (!virtualFile.isInLocalFileSystem()) {
+      return false;
+    }
+
+    File file = new File(virtualFile.getPath());
+    return !syncData.importResult.javaSourceFiles.contains(file);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java b/blaze-java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java
new file mode 100644
index 0000000..f31a694
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/ui/BlazeIntelliJProblemsView.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ui;
+
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.ui.BlazeProblemsView;
+import com.intellij.compiler.CompilerMessageImpl;
+import com.intellij.compiler.ProblemsView;
+import com.intellij.openapi.compiler.CompilerMessageCategory;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.vfs.VfsUtil;
+import com.intellij.openapi.vfs.VirtualFile;
+
+import java.util.UUID;
+
+public class BlazeIntelliJProblemsView implements BlazeProblemsView {
+  private final Project project;
+
+  private BlazeIntelliJProblemsView(Project project) {
+    this.project = project;
+  }
+
+  @Override
+  public void clearOldMessages(UUID sessionId) {
+    ProblemsView.SERVICE.getInstance(project).clearOldMessages(null, sessionId);
+  }
+
+  @Override
+  public void addMessage(IssueOutput issue, UUID sessionId) {
+    VirtualFile virtualFile = issue.getFile() != null
+                              ? VfsUtil.findFileByIoFile(issue.getFile(), true /* refresh */)
+                              : null;
+    CompilerMessageCategory category = issue.getCategory() == IssueOutput.Category.ERROR
+                                       ? CompilerMessageCategory.ERROR
+                                       : CompilerMessageCategory.WARNING;
+    CompilerMessageImpl message = new CompilerMessageImpl(
+      project,
+      category,
+      issue.getMessage(),
+      virtualFile,
+      issue.getLine(),
+      issue.getColumn(),
+      issue.getNavigatable()
+    );
+    ProblemsView.SERVICE.getInstance(project).addMessage(message, sessionId);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeImportJavaProjectWizard.java b/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeImportJavaProjectWizard.java
new file mode 100644
index 0000000..b00047d
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeImportJavaProjectWizard.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.java.wizard;
+
+import com.intellij.ide.util.newProjectWizard.AddModuleWizard;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.projectImport.ProjectImportProvider;
+
+final class BlazeImportJavaProjectWizard extends AddModuleWizard {
+  public BlazeImportJavaProjectWizard(VirtualFile file, ProjectImportProvider provider) {
+    super(null, file.getCanonicalPath(), provider);
+  }
+
+  @Override
+  protected String getDimensionServiceKey() {
+    return null; // No dimension service
+  }
+
+  public boolean runWizard() {
+    return showAndGet();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeImportNewJavaProjectAction.java b/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeImportNewJavaProjectAction.java
new file mode 100644
index 0000000..5c3c49f
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeImportNewJavaProjectAction.java
@@ -0,0 +1,95 @@
+/*
+ * 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.wizard;
+
+import com.google.idea.blaze.base.bazel.BuildSystemProvider;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.wizard.BlazeImportFileChooser;
+import com.intellij.ide.impl.NewProjectUtil;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Messages;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.projectImport.ProjectImportProvider;
+
+import javax.annotation.Nullable;
+import java.io.IOException;
+
+
+public class BlazeImportNewJavaProjectAction extends AnAction {
+  private static final Logger LOG = Logger.getInstance(BlazeImportNewJavaProjectAction.class);
+
+  public BlazeImportNewJavaProjectAction() {
+    super("Import Project...");
+  }
+
+  @Override
+  public void update(AnActionEvent e) {
+    super.update(e);
+    // this importer only supports importing blaze projects
+    if (!BuildSystemProvider.isBuildSystemAvailable(BuildSystem.Blaze)) {
+      e.getPresentation().setEnabledAndVisible(false);
+    } else {
+      e.getPresentation().setEnabledAndVisible(true);
+    }
+  }
+
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    try {
+      BlazeImportJavaProjectWizard wizard = selectFileAndCreateWizard();
+      if (wizard != null) {
+        if (!wizard.runWizard()) {
+          return;
+        }
+        //noinspection ConstantConditions
+        NewProjectUtil.createFromWizard(wizard, null);
+      }
+    }
+    catch (IOException | ConfigurationException exception) {
+      handleImportException(e.getProject(), exception);
+    }
+  }
+
+  @Nullable
+  private BlazeImportJavaProjectWizard selectFileAndCreateWizard() throws IOException, ConfigurationException {
+    VirtualFile fileToImport = BlazeImportFileChooser.getFileToImport();
+    if (fileToImport == null) {
+      return null;
+    }
+    return createImportWizard(fileToImport);
+  }
+
+  @Nullable
+  protected BlazeImportJavaProjectWizard createImportWizard(VirtualFile file) throws IOException, ConfigurationException {
+    ProjectImportProvider provider = createBlazeImportProvider();
+    return new BlazeImportJavaProjectWizard(file, provider);
+  }
+
+  private static ProjectImportProvider createBlazeImportProvider() {
+    BlazeNewJavaProjectImportBuilder builder = new BlazeNewJavaProjectImportBuilder();
+    return new BlazeNewProjectImportProvider(builder);
+  }
+
+  private static void handleImportException(@Nullable Project project, Exception e) {
+    String message = String.format("Project import failed: %s", e.getMessage());
+    Messages.showErrorDialog(project, message, "Import Project");
+    LOG.error(e);
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeNewJavaProjectImportBuilder.java b/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeNewJavaProjectImportBuilder.java
new file mode 100644
index 0000000..7378e4a
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeNewJavaProjectImportBuilder.java
@@ -0,0 +1,96 @@
+/*
+ * 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.wizard;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.wizard.BlazeNewProjectBuilder;
+import com.intellij.openapi.module.ModifiableModuleModel;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.JavaSdk;
+import com.intellij.openapi.projectRoots.SdkTypeId;
+import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
+import com.intellij.packaging.artifacts.ModifiableArtifactModel;
+import com.intellij.projectImport.ProjectImportBuilder;
+import icons.BlazeIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.util.List;
+
+/**
+ * Project builder for Blaze projects.
+ */
+public class BlazeNewJavaProjectImportBuilder extends ProjectImportBuilder<BlazeProjectData> {
+  private BlazeImportSettings importSettings;
+  private ProjectView projectView;
+
+  @NotNull
+  @Override
+  public String getName() {
+    return "Blaze";
+  }
+
+  @Override
+  public Icon getIcon() {
+    return BlazeIcons.Blaze;
+  }
+
+  @Override
+  public boolean isSuitableSdkType(SdkTypeId sdk) {
+    return sdk == JavaSdk.getInstance();
+  }
+
+  @Override
+  public List<BlazeProjectData> getList() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public boolean isMarked(BlazeProjectData element) {
+    return true;
+  }
+
+  @Override
+  public void setList(List<BlazeProjectData> gradleProjects) {
+  }
+
+  @Override
+  public void setOpenProjectSettingsAfter(boolean on) {
+  }
+
+  @Override
+  public List<Module> commit(final Project project,
+                             ModifiableModuleModel model,
+                             ModulesProvider modulesProvider,
+                             ModifiableArtifactModel artifactModel) {
+    assert importSettings != null;
+    assert projectView != null;
+
+    return BlazeNewProjectBuilder.commit(project, importSettings, projectView);
+  }
+
+  public void setImportSettings(BlazeImportSettings importSettings) {
+    this.importSettings = importSettings;
+  }
+
+  public void setProjectView(ProjectView projectView) {
+    this.projectView = projectView;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeNewProjectImportProvider.java b/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeNewProjectImportProvider.java
new file mode 100644
index 0000000..2ac630c
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard/BlazeNewProjectImportProvider.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.java.wizard;
+
+import com.google.idea.blaze.base.wizard.ImportSource;
+import com.intellij.ide.util.projectWizard.ModuleWizardStep;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.openapi.vfs.VirtualFile;
+import com.intellij.projectImport.ProjectImportProvider;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * The import provider for the Blaze plugin.
+ */
+public class BlazeNewProjectImportProvider extends ProjectImportProvider {
+
+  public BlazeNewProjectImportProvider(BlazeNewJavaProjectImportBuilder builder) {
+    super(builder);
+  }
+
+  @Override
+  protected boolean canImportFromFile(VirtualFile file) {
+    return ImportSource.canImport(file);
+  }
+
+  @Nullable
+  @Override
+  public String getFileSample() {
+    return "Workspace root, .blazeproject file, or BUILD file";
+  }
+
+  @Override
+  public String getPathToBeImported(VirtualFile file) {
+    return file.getCanonicalPath();
+  }
+
+  @Override
+  public ModuleWizardStep[] createSteps(WizardContext context) {
+    return new ModuleWizardStep[]{new SelectExternalProjectStep(context)};
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard/SelectExternalProjectStep.java b/blaze-java/src/com/google/idea/blaze/java/wizard/SelectExternalProjectStep.java
new file mode 100644
index 0000000..8d9ef36
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard/SelectExternalProjectStep.java
@@ -0,0 +1,107 @@
+/*
+ * 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.wizard;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard.BlazeProjectSettingsControl;
+import com.google.idea.blaze.base.wizard.ImportResults;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.projectImport.ProjectImportWizardStep;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+
+/**
+ * Handles the following responsibilities:
+ * <pre>
+ * <ul>
+ *   <li>allows end user to define external system config file to import from;</li>
+ *   <li>processes the input and reacts accordingly - shows error message if the project is invalid or proceeds to the next screen;</li>
+ * </ul>
+ * </pre>
+ *
+ * @author Denis Zhdanov
+ * @since 8/1/11 4:15 PM
+ */
+public class SelectExternalProjectStep extends ProjectImportWizardStep {
+
+  @NotNull
+  private final JPanel component = new JPanel(new BorderLayout());
+
+  @NotNull
+  private final BlazeProjectSettingsControl control;
+
+  private boolean settingsInitialised;
+
+  public SelectExternalProjectStep(@NotNull WizardContext context) {
+    super(context);
+    control = new BlazeProjectSettingsControl(context.getDisposable());
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialised) {
+      init();
+    }
+  }
+
+  private void init() {
+    BlazeNewJavaProjectImportBuilder builder = getBuilder();
+    File fileToImport = new File(builder.getFileToImport());
+    JPanel importControl = control.createComponent(fileToImport);
+    this.component.add(importControl);
+    settingsInitialised = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult validationResult = control.validate();
+    if (validationResult.error != null) {
+      throw new ConfigurationException(validationResult.error.getError());
+    }
+    return validationResult.success;
+  }
+
+  @Override
+  public void updateDataModel() {
+    ImportResults importResults = control.getResults();
+
+    BlazeNewJavaProjectImportBuilder builder = getBuilder();
+    WizardContext wizardContext = getWizardContext();
+
+    builder.setImportSettings(importResults.importSettings);
+    builder.setProjectView(importResults.projectView);
+    wizardContext.setProjectName(importResults.projectName);
+    wizardContext.setProjectFileDirectory(importResults.projectDataDirectory);
+  }
+
+  @Override
+  @NotNull
+  protected BlazeNewJavaProjectImportBuilder getBuilder() {
+    BlazeNewJavaProjectImportBuilder builder =
+      (BlazeNewJavaProjectImportBuilder)getWizardContext().getProjectBuilder();
+    assert builder != null;
+    return builder;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
new file mode 100644
index 0000000..b88ba19
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeEditProjectViewImportWizardStep.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.wizard2;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.BlazeProjectCommitException;
+import com.google.idea.blaze.base.wizard2.ui.BlazeEditProjectViewControl;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.projectImport.ProjectImportWizardStep;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+
+/**
+ * Shows the edit project view screen.
+ */
+class BlazeEditProjectViewImportWizardStep extends ProjectImportWizardStep {
+
+  private final JPanel component = new JPanel(new BorderLayout());
+  private BlazeEditProjectViewControl control;
+  private boolean settingsInitialised;
+
+  public BlazeEditProjectViewImportWizardStep(@NotNull WizardContext context) {
+    super(context);
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialised) {
+      init();
+    } else {
+      control.update(getProjectBuilder());
+    }
+  }
+
+  private void init() {
+    control = new BlazeEditProjectViewControl(getProjectBuilder(), getWizardContext().getDisposable());
+    this.component.add(control.getUiComponent());
+    settingsInitialised = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult validationResult = control.validate();
+    if (validationResult.error != null) {
+      throw new ConfigurationException(validationResult.error.getError());
+    }
+    return validationResult.success;
+  }
+
+  @Override
+  public void updateDataModel() {
+    BlazeNewProjectBuilder builder = getProjectBuilder();
+    control.updateBuilder(builder);
+
+    WizardContext wizardContext = getWizardContext();
+    wizardContext.setProjectName(builder.getProjectName());
+    wizardContext.setProjectFileDirectory(builder.getProjectDataDirectory());
+  }
+
+  @Override
+  public void onWizardFinished() throws CommitStepException {
+    try {
+      getProjectBuilder().commit();
+    }
+    catch (BlazeProjectCommitException e) {
+      throw new CommitStepException(e.getMessage());
+    }
+  }
+
+  @Override
+  public String getHelpId() {
+    return "docs/project-views.md";
+  }
+
+  private BlazeNewProjectBuilder getProjectBuilder() {
+    BlazeProjectImportBuilder builder = (BlazeProjectImportBuilder)getWizardContext().getProjectBuilder();
+    assert builder != null;
+    return builder.builder();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeImportProjectAction.java b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeImportProjectAction.java
new file mode 100644
index 0000000..ee3782d
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeImportProjectAction.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.wizard2;
+
+import com.google.idea.blaze.base.settings.Blaze;
+import com.intellij.ide.impl.NewProjectUtil;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+
+
+public class BlazeImportProjectAction extends AnAction {
+  @Override
+  public void actionPerformed(AnActionEvent e) {
+    BlazeNewProjectWizard wizard = new BlazeNewProjectWizard(
+      new BlazeNewProjectImportProvider(new BlazeProjectImportBuilder()));
+    if (!wizard.showAndGet()) {
+      return;
+    }
+    //noinspection ConstantConditions
+    NewProjectUtil.createFromWizard(wizard, null);
+  }
+
+  @Override
+  public void update(AnActionEvent e) {
+    super.update(e);
+    e.getPresentation().setText(String.format("Import %s Project...", Blaze.defaultBuildSystemName()));
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectImportProvider.java b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectImportProvider.java
new file mode 100644
index 0000000..5aa5683
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectImportProvider.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.wizard2;
+
+import com.intellij.ide.util.projectWizard.ModuleWizardStep;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.projectImport.ProjectImportProvider;
+
+/**
+ * The import provider for the Blaze plugin.
+ */
+class BlazeNewProjectImportProvider extends ProjectImportProvider {
+
+  public BlazeNewProjectImportProvider(BlazeProjectImportBuilder builder) {
+    super(builder);
+  }
+
+  @Override
+  public ModuleWizardStep[] createSteps(WizardContext context) {
+    return new ModuleWizardStep[]{
+      new BlazeSelectWorkspaceImportWizardStep(context),
+      new BlazeSelectBuildSystemBinaryStep(context),
+      new BlazeSelectProjectViewImportWizardStep(context),
+      new BlazeEditProjectViewImportWizardStep(context)
+    };
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java
new file mode 100644
index 0000000..22fb231
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeNewProjectWizard.java
@@ -0,0 +1,55 @@
+/*
+ * 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.wizard2;
+
+import com.google.idea.blaze.base.help.BlazeHelpHandler;
+import com.intellij.ide.util.newProjectWizard.AddModuleWizard;
+import com.intellij.projectImport.ProjectImportProvider;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.event.ActionListener;
+
+final class BlazeNewProjectWizard extends AddModuleWizard {
+  public BlazeNewProjectWizard(ProjectImportProvider provider) {
+    super(null, null, provider);
+  }
+
+  @Override
+  protected String getDimensionServiceKey() {
+    return null; // No dimension service
+  }
+
+  @Override
+  protected void helpAction() {
+    doHelpAction();
+  }
+
+  @Override
+  protected void doHelpAction() {
+    String helpId = getHelpID();
+    BlazeHelpHandler helpHandler = BlazeHelpHandler.getInstance();
+    if (helpId != null && helpHandler != null) {
+      helpHandler.handleHelp(helpId);
+    }
+  }
+
+  // Swallow the escape key
+  @Nullable
+  @Override
+  protected ActionListener createCancelAction() {
+    return null;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeProjectImportBuilder.java b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeProjectImportBuilder.java
new file mode 100644
index 0000000..5e02a22
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeProjectImportBuilder.java
@@ -0,0 +1,88 @@
+/*
+ * 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.wizard2;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.intellij.openapi.module.ModifiableModuleModel;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.JavaSdk;
+import com.intellij.openapi.projectRoots.SdkTypeId;
+import com.intellij.openapi.roots.ui.configuration.ModulesProvider;
+import com.intellij.packaging.artifacts.ModifiableArtifactModel;
+import com.intellij.projectImport.ProjectImportBuilder;
+import icons.BlazeIcons;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.util.List;
+
+/**
+ * Wrapper around a {@link BlazeNewProjectBuilder} to fit into
+ * IntelliJ's import framework.
+ */
+class BlazeProjectImportBuilder extends ProjectImportBuilder<Void> {
+  private BlazeNewProjectBuilder builder = new BlazeNewProjectBuilder();
+
+  @NotNull
+  @Override
+  public String getName() {
+    return Blaze.defaultBuildSystemName();
+  }
+
+  @Override
+  public Icon getIcon() {
+    return BlazeIcons.Blaze;
+  }
+
+  @Override
+  public boolean isSuitableSdkType(SdkTypeId sdk) {
+    return sdk == JavaSdk.getInstance();
+  }
+
+  @Override
+  public List<Void> getList() {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public boolean isMarked(Void element) {
+    return true;
+  }
+
+  @Override
+  public void setList(List<Void> gradleProjects) {
+  }
+
+  @Override
+  public void setOpenProjectSettingsAfter(boolean on) {
+  }
+
+  @Override
+  public List<Module> commit(final Project project,
+                             ModifiableModuleModel model,
+                             ModulesProvider modulesProvider,
+                             ModifiableArtifactModel artifactModel) {
+    builder.commitToProject(project);
+    return ImmutableList.of();
+  }
+
+  public BlazeNewProjectBuilder builder() {
+    return builder;
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectBuildSystemBinaryStep.java b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectBuildSystemBinaryStep.java
new file mode 100644
index 0000000..940fc6c
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectBuildSystemBinaryStep.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.wizard2;
+
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.ui.SelectBazelBinaryControl;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.projectImport.ProjectImportWizardStep;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+
+class BlazeSelectBuildSystemBinaryStep extends ProjectImportWizardStep {
+
+  private final JPanel component = new JPanel(new BorderLayout());
+  private SelectBazelBinaryControl control;
+  private boolean settingsInitialized = false;
+
+  public BlazeSelectBuildSystemBinaryStep(@NotNull WizardContext context) {
+    super(context);
+  }
+
+  @Override
+  public boolean isStepVisible() {
+    updateStep();
+    if (control.builder.getBuildSystem() != BuildSystem.Bazel) {
+      return false;
+    }
+    String currentBinaryPath = BlazeUserSettings.getInstance().getBazelBinaryPath();
+    return currentBinaryPath == null;
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialized) {
+      init();
+    }
+  }
+
+  private void init() {
+    control = new SelectBazelBinaryControl(getProjectBuilder());
+    component.add(control.getUiComponent());
+    settingsInitialized = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult result = control.validate();
+    if (!result.success) {
+      throw new ConfigurationException(result.error.getError());
+    }
+    return true;
+  }
+
+  @Override
+  public void updateDataModel() {
+  }
+
+  @Override
+  public void onWizardFinished() throws CommitStepException {
+    control.commit();
+  }
+
+  private BlazeNewProjectBuilder getProjectBuilder() {
+    BlazeProjectImportBuilder builder = (BlazeProjectImportBuilder)getWizardContext().getProjectBuilder();
+    assert builder != null;
+    return builder.builder();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectProjectViewImportWizardStep.java b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectProjectViewImportWizardStep.java
new file mode 100644
index 0000000..14ba13e
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectProjectViewImportWizardStep.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.wizard2;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.ui.BlazeSelectProjectViewControl;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.projectImport.ProjectImportWizardStep;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+
+class BlazeSelectProjectViewImportWizardStep extends ProjectImportWizardStep {
+
+  private final JPanel component = new JPanel(new BorderLayout());
+  private BlazeSelectProjectViewControl control;
+  private boolean settingsInitialised;
+
+  public BlazeSelectProjectViewImportWizardStep(@NotNull WizardContext context) {
+    super(context);
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialised) {
+      init();
+    } else {
+      control.update(getProjectBuilder());
+    }
+  }
+
+  private void init() {
+    control = new BlazeSelectProjectViewControl(getProjectBuilder());
+    this.component.add(control.getUiComponent());
+    settingsInitialised = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult result = control.validate();
+    if (!result.success) {
+      throw new ConfigurationException(result.error.getError());
+    }
+    return true;
+  }
+
+  @Override
+  public void updateDataModel() {
+    control.updateBuilder(getProjectBuilder());
+  }
+
+  @Override
+  public void onWizardFinished() throws CommitStepException {
+    control.commit();
+  }
+
+  @Override
+  public String getHelpId() {
+    return "docs/project-views.md";
+  }
+
+  private BlazeNewProjectBuilder getProjectBuilder() {
+    BlazeProjectImportBuilder builder = (BlazeProjectImportBuilder)getWizardContext().getProjectBuilder();
+    assert builder != null;
+    return builder.builder();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectWorkspaceImportWizardStep.java b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectWorkspaceImportWizardStep.java
new file mode 100644
index 0000000..2923643
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/BlazeSelectWorkspaceImportWizardStep.java
@@ -0,0 +1,87 @@
+/*
+ * 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.wizard2;
+
+import com.google.idea.blaze.base.ui.BlazeValidationResult;
+import com.google.idea.blaze.base.wizard2.BlazeNewProjectBuilder;
+import com.google.idea.blaze.base.wizard2.ui.BlazeSelectWorkspaceControl;
+import com.intellij.ide.util.projectWizard.WizardContext;
+import com.intellij.ide.wizard.CommitStepException;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.projectImport.ProjectImportWizardStep;
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.*;
+import java.awt.*;
+
+class BlazeSelectWorkspaceImportWizardStep extends ProjectImportWizardStep {
+
+  private final JPanel component = new JPanel(new BorderLayout());
+  private BlazeSelectWorkspaceControl control;
+  private boolean settingsInitialised;
+
+  public BlazeSelectWorkspaceImportWizardStep(@NotNull WizardContext context) {
+    super(context);
+  }
+
+  @Override
+  public JComponent getComponent() {
+    return component;
+  }
+
+  @Override
+  public void updateStep() {
+    if (!settingsInitialised) {
+      init();
+    }
+  }
+
+  private void init() {
+    control = new BlazeSelectWorkspaceControl(getProjectBuilder());
+    this.component.add(control.getUiComponent());
+    settingsInitialised = true;
+  }
+
+  @Override
+  public boolean validate() throws ConfigurationException {
+    BlazeValidationResult result = control.validate();
+    if (!result.success) {
+      throw new ConfigurationException(result.error.getError());
+    }
+    return true;
+  }
+
+  @Override
+  public void updateDataModel() {
+    control.updateBuilder(getProjectBuilder());
+  }
+
+  @Override
+  public void onWizardFinished() throws CommitStepException {
+    control.commit();
+  }
+
+  @Override
+  public String getHelpId() {
+    return "docs/import-project.md";
+  }
+
+  private BlazeNewProjectBuilder getProjectBuilder() {
+    BlazeProjectImportBuilder builder = (BlazeProjectImportBuilder)getWizardContext().getProjectBuilder();
+    assert builder != null;
+    return builder.builder();
+  }
+}
diff --git a/blaze-java/src/com/google/idea/blaze/java/wizard2/README b/blaze-java/src/com/google/idea/blaze/java/wizard2/README
new file mode 100644
index 0000000..ae0353d
--- /dev/null
+++ b/blaze-java/src/com/google/idea/blaze/java/wizard2/README
@@ -0,0 +1,2 @@
+This package wraps the base.wizard classes in the IntelliJ project
+import wizard framework.
\ No newline at end of file
diff --git a/blaze-java/tests/integrationtests/com/google/idea/blaze/java/lang/build/JavaClassRenameTest.java b/blaze-java/tests/integrationtests/com/google/idea/blaze/java/lang/build/JavaClassRenameTest.java
new file mode 100644
index 0000000..b4f6d18
--- /dev/null
+++ b/blaze-java/tests/integrationtests/com/google/idea/blaze/java/lang/build/JavaClassRenameTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.lang.build;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.intellij.psi.PsiJavaFile;
+import com.intellij.refactoring.rename.RenameProcessor;
+
+/**
+ * Tests that BUILD file references are correctly updated when performing rename refactors.
+ */
+public class JavaClassRenameTest extends BuildFileIntegrationTestCase {
+
+  public void testRenameJavaClass() {
+    PsiJavaFile javaFile = (PsiJavaFile) createPsiFile(
+        "com/google/foo/JavaClass.java",
+        "package com.google.foo;",
+        "public class JavaClass {}");
+
+    BuildFile buildFile = createBuildFile(
+        "com/google/foo/BUILD",
+        "java_library(name = \"ref2\", srcs = [\"JavaClass.java\"])");
+
+    new RenameProcessor(getProject(), javaFile.getClasses()[0], "NewName", false, false).run();
+
+    assertFileContents(
+        buildFile,
+        "java_library(name = \"ref2\", srcs = [\"NewName.java\"])");
+  }
+
+}
diff --git a/blaze-java/tests/integrationtests/com/google/idea/blaze/java/lang/build/SafeDeleteTest.java b/blaze-java/tests/integrationtests/com/google/idea/blaze/java/lang/build/SafeDeleteTest.java
new file mode 100644
index 0000000..ab9390f
--- /dev/null
+++ b/blaze-java/tests/integrationtests/com/google/idea/blaze/java/lang/build/SafeDeleteTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.lang.build;
+
+import com.google.idea.blaze.base.lang.buildfile.BuildFileIntegrationTestCase;
+import com.google.idea.blaze.base.lang.buildfile.psi.BuildFile;
+import com.google.idea.blaze.base.lang.buildfile.psi.util.PsiUtils;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.refactoring.BaseRefactoringProcessor;
+import com.intellij.refactoring.safeDelete.SafeDeleteHandler;
+
+/**
+ * Tests for the safe delete action which aren't covered by existing tests.
+ */
+public class SafeDeleteTest extends BuildFileIntegrationTestCase {
+
+  public void testIndirectGlobReferencesNotIncluded() {
+    PsiFile javaFile = createPsiFile(
+      "com/google/Test.java",
+      "package com.google;",
+      "public class Test {}");
+
+    PsiClass javaClass = PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiClass.class);
+
+    BuildFile buildFile = createBuildFile(
+      "com/google/BUILD",
+      "java_library(",
+      "    name = 'lib'",
+      "    srcs = glob(['*.java'])",
+      ")");
+
+    try {
+      SafeDeleteHandler.invoke(getProject(), new PsiElement[] {javaClass}, true);
+    } catch (BaseRefactoringProcessor.ConflictsInTestsException e) {
+      fail("Glob reference was incorrectly included");
+      return;
+    }
+  }
+
+  public void testDirectGlobReferencesIncluded() {
+    PsiFile javaFile = createPsiFile(
+      "com/google/Test.java",
+      "package com.google;",
+      "public class Test {}");
+
+    PsiClass javaClass = PsiUtils.findFirstChildOfClassRecursive(javaFile, PsiClass.class);
+
+    BuildFile buildFile = createBuildFile(
+      "com/google/BUILD",
+      "java_library(",
+      "    name = 'lib'",
+      "    srcs = glob(['Test.java'])",
+      ")");
+
+    try {
+      SafeDeleteHandler.invoke(getProject(), new PsiElement[] {javaClass}, true);
+    } catch (BaseRefactoringProcessor.ConflictsInTestsException expected) {
+      return;
+    }
+    fail("Expected an unsafe usage to be found");
+  }
+
+
+}
diff --git a/blaze-java/tests/integrationtests/com/google/idea/blaze/java/sync/JavaSyncTest.java b/blaze-java/tests/integrationtests/com/google/idea/blaze/java/sync/JavaSyncTest.java
new file mode 100644
index 0000000..52998b1
--- /dev/null
+++ b/blaze-java/tests/integrationtests/com/google/idea/blaze/java/sync/JavaSyncTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.JavaRuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.sync.BlazeSyncIntegrationTestCase;
+import com.google.idea.blaze.base.sync.BlazeSyncParams;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
+import com.google.idea.blaze.java.sync.model.BlazeJavaSyncData;
+import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Java-specific sync integration tests.
+ */
+public class JavaSyncTest extends BlazeSyncIntegrationTestCase {
+
+  public void testJavaClassesPresentInClassPath() throws Exception {
+    setProjectView(
+      "directories:",
+      "  java/com/google",
+      "targets:",
+      "  //java/com/google:lib",
+      "workspace_type: java"
+    );
+
+    createWorkspaceFile(
+      "java/com/google/ClassWithUniqueName1.java",
+      "package com.google;",
+      "public class ClassWithUniqueName1 {}"
+    );
+
+    createWorkspaceFile(
+      "java/com/google/ClassWithUniqueName2.java",
+      "package com.google;",
+      "public class ClassWithUniqueName2 {}"
+    );
+
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = RuleMapBuilder.builder()
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                 .setLabel("//java/com/google:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("java/com/google/ClassWithUniqueName1.java"))
+                 .addSource(sourceRoot("java/com/google/ClassWithUniqueName2.java"))
+                 .setJavaInfo(JavaRuleIdeInfo.builder()))
+      .build();
+
+    setRuleMap(ruleMap);
+
+    BlazeSyncParams syncParams = new BlazeSyncParams.Builder("Full Sync", BlazeSyncParams.SyncMode.FULL).build();
+    runBlazeSync(syncParams);
+
+    assertNoErrors();
+
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    assertThat(blazeProjectData).isNotNull();
+    assertThat(blazeProjectData.ruleMap).isEqualTo(ruleMap);
+    assertThat(blazeProjectData.workspaceLanguageSettings.getWorkspaceType())
+      .isEqualTo(WorkspaceType.JAVA);
+
+    BlazeJavaSyncData javaSyncData = blazeProjectData.syncState.get(BlazeJavaSyncData.class);
+    List<BlazeContentEntry> contentEntries = javaSyncData.importResult.contentEntries;
+    assertThat(contentEntries).hasSize(1);
+
+    BlazeContentEntry contentEntry = contentEntries.get(0);
+    assertThat(contentEntry.contentRoot.getPath()).isEqualTo(tempDirectory.getPath() + "/java/com/google");
+    assertThat(contentEntry.sources).hasSize(1);
+
+    BlazeSourceDirectory sourceDir = contentEntry.sources.get(0);
+    assertThat(sourceDir.getPackagePrefix()).isEqualTo("com.google");
+    assertThat(sourceDir.getDirectory().getPath()).isEqualTo(tempDirectory.getPath() + "/java/com/google");
+  }
+
+}
diff --git a/blaze-java/tests/unittests/com/google/idea/blaze/java/run/BlazeCommandRunConfigurationTest.java b/blaze-java/tests/unittests/com/google/idea/blaze/java/run/BlazeCommandRunConfigurationTest.java
new file mode 100644
index 0000000..07a59b2
--- /dev/null
+++ b/blaze-java/tests/unittests/com/google/idea/blaze/java/run/BlazeCommandRunConfigurationTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.java.run.BlazeCommandRunConfiguration.BlazeCommandRunConfigurationSettingsEditor;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.util.Disposer;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import org.jdom.Element;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for {@link BlazeCommandRunConfiguration}.
+ */
+@RunWith(JUnit4.class)
+public class BlazeCommandRunConfigurationTest extends BlazeTestCase {
+  private static Label label;
+  private static final BlazeCommandName COMMAND = BlazeCommandName.fromString("command");
+  private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS = new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+
+  BlazeCommandRunConfigurationType type = new BlazeCommandRunConfigurationType();
+  BlazeCommandRunConfiguration configuration;
+
+
+  @Override
+  protected void initTest(
+    @NotNull Container applicationServices,
+    @NotNull Container projectServices) {
+    super.initTest(applicationServices, projectServices);
+
+    applicationServices.register(ExperimentService.class, new MockExperimentService());
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
+
+    configuration = type.getFactory().createTemplateConfiguration(project);
+    label = new Label("//package:rule");
+  }
+
+  @Test
+  public void readAndWriteShouldMatch() throws InvalidDataException, WriteExternalException {
+    configuration.setTarget(label);
+    configuration.setCommand(COMMAND);
+    configuration.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    configuration.setExeFlags(ImmutableList.of("--exeFlag1"));
+    Element element = new Element("test");
+    configuration.writeExternal(element);
+    BlazeCommandRunConfiguration readConfiguration =
+      type.getFactory().createTemplateConfiguration(project);
+    readConfiguration.readExternal(element);
+    assertThat(readConfiguration.getTarget()).isEqualTo(label);
+    assertThat(readConfiguration.getCommand()).isEqualTo(COMMAND);
+    assertThat(readConfiguration.getAllBlazeFlags()).isEqualTo(ImmutableList.of("--flag1", "--flag2"));
+    assertThat(readConfiguration.getAllExeFlags()).isEqualTo(ImmutableList.of("--exeFlag1"));
+  }
+
+  @Test
+  public void readAndWriteShouldHandleNulls() throws InvalidDataException, WriteExternalException {
+    Element element = new Element("test");
+    configuration.writeExternal(element);
+    BlazeCommandRunConfiguration readConfiguration =
+      type.getFactory().createTemplateConfiguration(project);
+    readConfiguration.readExternal(element);
+    assertThat(readConfiguration.getTarget()).isEqualTo(configuration.getTarget());
+    assertThat(readConfiguration.getCommand()).isEqualTo(configuration.getCommand());
+    assertThat(readConfiguration.getAllBlazeFlags()).isEqualTo(configuration.getAllBlazeFlags());
+    assertThat(readConfiguration.getAllExeFlags()).isEqualTo(configuration.getAllExeFlags());
+  }
+
+  @Test
+  public void readShouldOmitEmptyFlags() throws InvalidDataException, WriteExternalException {
+    configuration.setBlazeFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
+    configuration.setExeFlags(Lists.newArrayList("hi ", "", "I'm", " ", "\t", "Josh\r\n", "\n"));
+    Element element = new Element("test");
+    configuration.writeExternal(element);
+    BlazeCommandRunConfiguration readConfiguration =
+      type.getFactory().createTemplateConfiguration(project);
+    readConfiguration.readExternal(element);
+    assertThat(readConfiguration.getAllBlazeFlags()).isEqualTo(ImmutableList.of("hi", "I'm", "Josh"));
+    assertThat(readConfiguration.getAllExeFlags()).isEqualTo(ImmutableList.of("hi", "I'm", "Josh"));
+  }
+
+  @Test
+  public void editorApplyToAndResetFromShouldHandleNulls() throws ConfigurationException {
+    BlazeCommandRunConfigurationSettingsEditor editor =
+      new BlazeCommandRunConfigurationSettingsEditor("Blaze");
+    editor.resetFrom(configuration);
+    BlazeCommandRunConfiguration readConfiguration =
+      type.getFactory().createTemplateConfiguration(project);
+    editor.applyEditorTo(readConfiguration);
+    assertThat(readConfiguration.getTarget()).isEqualTo(configuration.getTarget());
+    assertThat(readConfiguration.getCommand()).isEqualTo(configuration.getCommand());
+    assertThat(readConfiguration.getAllBlazeFlags()).isEqualTo(configuration.getAllBlazeFlags());
+    assertThat(readConfiguration.getAllExeFlags()).isEqualTo(configuration.getAllExeFlags());
+
+    Disposer.dispose(editor);
+  }
+
+  @Test
+  public void editorApplyToAndResetFromShouldMatch() throws ConfigurationException {
+    BlazeCommandRunConfigurationSettingsEditor editor = new BlazeCommandRunConfigurationSettingsEditor("Blaze");
+    configuration.setTarget(label);
+    configuration.setCommand(COMMAND);
+    configuration.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    configuration.setExeFlags(ImmutableList.of("--exeFlag1", "--exeFlag2"));
+    editor.resetFrom(configuration);
+
+    BlazeCommandRunConfiguration readConfiguration = type.getFactory().createTemplateConfiguration(project);
+    editor.applyEditorTo(readConfiguration);
+    assertThat(readConfiguration.getTarget()).isEqualTo(configuration.getTarget());
+    assertThat(readConfiguration.getCommand()).isEqualTo(configuration.getCommand());
+    assertThat(readConfiguration.getAllBlazeFlags()).isEqualTo(configuration.getAllBlazeFlags());
+    assertThat(readConfiguration.getAllExeFlags()).isEqualTo(configuration.getAllExeFlags());
+    
+    Disposer.dispose(editor);
+  }
+}
diff --git a/blaze-java/tests/unittests/com/google/idea/blaze/java/run/BlazeCommandRunProfileStateTest.java b/blaze-java/tests/unittests/com/google/idea/blaze/java/run/BlazeCommandRunProfileStateTest.java
new file mode 100644
index 0000000..03a0a7f
--- /dev/null
+++ b/blaze-java/tests/unittests/com/google/idea/blaze/java/run/BlazeCommandRunProfileStateTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.run;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.command.BuildFlagsProvider;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.intellij.openapi.project.Project;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Tests for {@link BlazeCommandRunProfileState}.
+ */
+@RunWith(JUnit4.class)
+public class BlazeCommandRunProfileStateTest extends BlazeTestCase {
+
+  private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS = new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+
+  private BlazeCommandRunConfiguration configuration;
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
+
+    configuration = new BlazeCommandRunConfigurationType().getFactory().createTemplateConfiguration(project);
+
+    ExperimentService experimentService = new MockExperimentService();
+    applicationServices.register(ExperimentService.class, experimentService);
+    applicationServices.register(RuleFinder.class, new MockRuleFinder());
+    applicationServices.register(BlazeUserSettings.class, new BlazeUserSettings());
+    registerExtensionPoint(BuildFlagsProvider.EP_NAME, BuildFlagsProvider.class);
+  }
+
+  @Test
+  public void flagsShouldBeAppendedIfPresent() {
+    configuration.setTarget(new Label("//label:rule"));
+    configuration.setCommand(BlazeCommandName.fromString("command"));
+    configuration.setBlazeFlags(ImmutableList.of("--flag1", "--flag2"));
+    assertThat(
+      BlazeCommandRunProfileState.getBlazeCommand(project, configuration, ProjectViewSet.builder().build(), false /* debug */).toList())
+      .isEqualTo(ImmutableList.of(
+        "/usr/bin/blaze",
+        "command",
+        BlazeFlags.getToolTagFlag(),
+        "--flag1",
+        "--flag2",
+        "--",
+        "//label:rule"
+      ));
+  }
+
+  @Test
+  public void debugFlagShouldBeIncludedForJavaTest() {
+    configuration.setTarget(new Label("//label:rule"));
+    configuration.setCommand(BlazeCommandName.fromString("command"));
+    assertThat(
+      BlazeCommandRunProfileState.getBlazeCommand(project, configuration, ProjectViewSet.builder().build(), true /* debug */).toList())
+      .isEqualTo(ImmutableList.of(
+        "/usr/bin/blaze",
+        "command",
+        BlazeFlags.getToolTagFlag(),
+        "--java_debug",
+        "--",
+        "//label:rule"
+      ));
+  }
+
+  @Test
+  public void debugFlagShouldBeIncludedForJavaBinary() {
+    configuration.setTarget(new Label("//label:java_binary_rule"));
+    configuration.setCommand(BlazeCommandName.fromString("command"));
+    assertThat(
+      BlazeCommandRunProfileState.getBlazeCommand(project, configuration, ProjectViewSet.builder().build(), true /* debug */).toList())
+      .isEqualTo(ImmutableList.of(
+        "/usr/bin/blaze",
+        "command",
+        BlazeFlags.getToolTagFlag(),
+        "--",
+        "//label:java_binary_rule",
+        "--debug"
+      ));
+  }
+
+  private static class MockRuleFinder extends RuleFinder {
+    @Override
+    public List<RuleIdeInfo> findRules(Project project, Predicate<RuleIdeInfo> predicate) {
+      return null;
+    }
+
+    @Override
+    public RuleIdeInfo ruleForTarget(Project project, final Label target) {
+      RuleIdeInfo.Builder builder = RuleIdeInfo.builder().setLabel(target);
+      if (target.ruleName().toString().equals("java_binary_rule")) {
+        builder.setKind(Kind.JAVA_BINARY);
+      } else {
+        builder.setKind(Kind.JAVA_TEST);
+      }
+      return builder.build();
+    }
+  }
+}
diff --git a/blaze-java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java b/blaze-java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
new file mode 100644
index 0000000..1393cb1
--- /dev/null
+++ b/blaze-java/tests/unittests/com/google/idea/blaze/java/sync/importer/BlazeJavaWorkspaceImporterTest.java
@@ -0,0 +1,1146 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.java.sync.importer;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.executor.MockBlazeExecutor;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.ideinfo.*;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.prefetch.MockPrefetchService;
+import com.google.idea.blaze.base.prefetch.PrefetchService;
+import com.google.idea.blaze.base.projectview.ProjectView;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.projectview.section.Glob;
+import com.google.idea.blaze.base.projectview.section.ListSection;
+import com.google.idea.blaze.base.projectview.section.sections.*;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.ErrorCollector;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.settings.Blaze.BuildSystem;
+import com.google.idea.blaze.base.settings.BlazeImportSettings;
+import com.google.idea.blaze.base.settings.BlazeImportSettingsManager;
+import com.google.idea.blaze.base.sync.workspace.ArtifactLocationDecoder;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.java.sync.jdeps.JdepsMap;
+import com.google.idea.blaze.java.sync.model.*;
+import com.google.idea.blaze.java.sync.source.JavaSourcePackageReader;
+import com.google.idea.blaze.java.sync.source.PackageManifestReader;
+import com.google.idea.blaze.java.sync.source.SourceArtifact;
+import com.google.idea.blaze.java.sync.workingset.JavaWorkingSet;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for BlazeJavaWorkspaceImporter
+ */
+public class BlazeJavaWorkspaceImporterTest extends BlazeTestCase {
+
+  private String FAKE_WORKSPACE_ROOT = "/root";
+  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File(FAKE_WORKSPACE_ROOT));
+
+  private static final String FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT =
+    "blaze-out/gcc-4.X.Y-crosstool-v17-hybrid-grtev3-k8-fastbuild/bin";
+
+  private static final String FAKE_GEN_ROOT =
+    "/path/to/8093958afcfde6c33d08b621dfaa4e09/root/"
+    + FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT;
+
+  private static final ArtifactLocationDecoder FAKE_ARTIFACT_DECODER = new ArtifactLocationDecoder(
+    new BlazeRoots(
+      new File("/"),
+      ImmutableList.of(),
+      new ExecutionRootPath("out/crosstool/bin"),
+      new ExecutionRootPath("out/crosstool/gen")
+    ),
+    null
+  );
+
+  private static final BlazeImportSettings DUMMY_IMPORT_SETTINGS = new BlazeImportSettings("", "", "", "", "", BuildSystem.Blaze);
+
+  private static class JdepsMock implements JdepsMap {
+    Map<Label, List<String>> jdeps = Maps.newHashMap();
+
+    @Nullable
+    @Override
+    public List<String> getDependenciesForRule(@NotNull Label label) {
+      return jdeps.get(label);
+    }
+
+    JdepsMock put(Label label, List<String> values) {
+      jdeps.put(label, values);
+      return this;
+    }
+  }
+
+  private BlazeContext context;
+  private ErrorCollector errorCollector = new ErrorCollector();
+  private final JdepsMock jdepsMap = new JdepsMock();
+  private JavaWorkingSet workingSet = null;
+  private MockExperimentService experimentService;
+
+  @Override
+  protected void initTest(@NotNull Container applicationServices, @NotNull Container projectServices) {
+    experimentService = new MockExperimentService();
+    applicationServices.register(ExperimentService.class, experimentService);
+
+    BlazeExecutor blazeExecutor = new MockBlazeExecutor();
+    applicationServices.register(BlazeExecutor.class, blazeExecutor);
+    projectServices.register(BlazeImportSettingsManager.class, new BlazeImportSettingsManager(project));
+    BlazeImportSettingsManager.getInstance(getProject()).setImportSettings(DUMMY_IMPORT_SETTINGS);
+
+    // will silently fall back to FilePathJavaPackageReader
+    applicationServices.register(
+      JavaSourcePackageReader.class,
+      new JavaSourcePackageReader() {
+        @Nullable
+        @Override
+        public String getDeclaredPackageOfJavaFile(@NotNull BlazeContext context, @NotNull SourceArtifact sourceArtifact) {
+          return null;
+        }
+      }
+    );
+    applicationServices.register(PackageManifestReader.class, new PackageManifestReader());
+    applicationServices.register(PrefetchService.class, new MockPrefetchService());
+
+    context = new BlazeContext();
+    context.addOutputSink(IssueOutput.class, errorCollector);
+  }
+
+  BlazeJavaImportResult importWorkspace(
+    WorkspaceRoot workspaceRoot,
+    RuleMapBuilder ruleMapBuilder,
+    ProjectView projectView) {
+
+    ProjectViewSet projectViewSet = ProjectViewSet.builder().add(projectView).build();
+
+    BlazeJavaWorkspaceImporter blazeWorkspaceImporter = new BlazeJavaWorkspaceImporter(
+      project,
+      workspaceRoot,
+      projectViewSet,
+      ruleMapBuilder.build(),
+      jdepsMap,
+      workingSet,
+      FAKE_ARTIFACT_DECODER
+    );
+
+    return blazeWorkspaceImporter.importWorkspace(context);
+  }
+
+  /**
+   * Ensure an empty response results in an empty import result.
+   */
+  @Test
+  public void testEmptyProject() {
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      RuleMapBuilder.builder(),
+      ProjectView.builder().build()
+    );
+    errorCollector.assertNoIssues();
+    assertTrue(result.contentEntries.isEmpty());
+  }
+
+  @Test
+  public void testSingleModule() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/subdir/SubdirHelper.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/example_debug-ijar.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/example_debug.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertEquals(1, result.buildOutputJars.size());
+    File compilerOutputLib = result.buildOutputJars.iterator().next();
+    assertNotNull(compilerOutputLib);
+    assertTrue(compilerOutputLib.getPath().endsWith("example_debug.jar"));
+
+    assertThat(result.contentEntries).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google/android/apps/example")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/android/apps/example")
+                     .setPackagePrefix("com.google.android.apps.example")
+                     .build())
+        .build()
+    );
+
+    assertThat(result.javaSourceFiles).containsExactly(
+      sourceRoot("java/com/google/android/apps/example/MainActivity.java").getFile(),
+      sourceRoot("java/com/google/android/apps/example/subdir/SubdirHelper.java").getFile()
+    );
+  }
+
+  @Test
+  public void testGeneratedLibrariesIncluded() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/example"))))
+             .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/example:lib")
+          .setBuildFile(sourceRoot("java/com/google/example/BUILD"))
+          .setKind("java_library")
+          .addSource(sourceRoot("java/com/google/example/Test.java"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/example/lib-ijar.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/example/lib.jar")))
+                         .addGeneratedJar(LibraryArtifact.builder()
+                                            .setJar(genRoot("java/com/google/example/lib-gen.jar"))
+                                            .setRuntimeJar(genRoot("java/com/google/example/lib-gen.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    assertThat(result.libraries.values().stream().map(BlazeJavaWorkspaceImporterTest::libraryFileName).collect(Collectors.toList()))
+      .containsExactly("lib-gen.jar");
+  }
+
+
+  /**
+   * Imports two binaries and a library. Only one binary should pass the package filter.
+   */
+  @Test
+  public void testImportFilter() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .addDependency("//java/com/google/android/libraries/example:example")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/example_debug.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/example_debug.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/libraries/example:example")
+          .setBuildFile(sourceRoot("java/com/google/android/libraries/example/BUILD"))
+          .setKind("android_library")
+          .addSource(sourceRoot("java/com/google/android/libraries/example/SharedActivity.java"))
+          .setAndroidInfo(
+            AndroidRuleIdeInfo.builder()
+              .setManifestFile(sourceRoot("java/com/google/android/libraries/example/AndroidManifest.xml"))
+              .addResource(sourceRoot("java/com/google/android/libraries/example/res"))
+              .setGenerateResourceClass(true)
+              .setResourceJavaPackage("com.google.android.libraries.example"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/libraries/example/example.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/libraries/example/example.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/dontimport:example_debug")
+          .setBuildFile(sourceRoot("java/com/dontimport/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/dontimport/MainActivity.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/dontimport/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/dontimport/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.dontimport"))
+          .addDependency("//java/com/dontimport:sometarget")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/dontimport/example_debug.jar"))
+                                   .setRuntimeJar(genRoot("java/com/dontimport/example_debug.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.contentEntries).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google/android/apps/example")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/android/apps/example")
+                     .setPackagePrefix("com.google.android.apps.example")
+                     .build())
+        .build()
+    );
+    assertThat(result.javaSourceFiles).containsExactly(
+      sourceRoot("java/com/google/android/apps/example/MainActivity.java").getFile()
+    );
+  }
+
+  /**
+   * Import a project and its tests
+   */
+  @Test
+  public void testProjectAndTests() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("javatests/com/google/android/apps/example"))))
+      .put(ListSection.builder(TestSourceSection.KEY).add(new Glob("javatests/*")))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/subdir/SubdirHelper.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/example_debug.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/example_debug.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//javatests/com/google/android/apps/example:example")
+          .setBuildFile(sourceRoot("javatests/com/google/android/apps/example/BUILD"))
+          .setKind("android_test")
+          .addSource(sourceRoot("javatests/com/google/android/apps/example/ExampleTests.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .addDependency("//java/com/google/android/apps/example:example_debug")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("javatests/com/google/android/apps/example/example.jar"))
+                                   .setRuntimeJar(genRoot("javatests/com/google/android/apps/example/example.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.contentEntries).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google/android/apps/example")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/android/apps/example")
+                     .setPackagePrefix("com.google.android.apps.example")
+                     .build())
+        .build(),
+      BlazeContentEntry.builder("/root/javatests/com/google/android/apps/example")
+        .addSource(BlazeSourceDirectory.builder("/root/javatests/com/google/android/apps/example")
+                     .setPackagePrefix("com.google.android.apps.example")
+                     .setTest(true)
+                     .build())
+        .build()
+    );
+  }
+
+  /**
+   * Test library with a source jar
+   */
+  @Test
+  public void testLibraryWithSourceJar() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("javatests/com/google/android/apps/example"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/subdir/SubdirHelper.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(genRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .addDependency("//thirdparty/some/library:library")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/example_debug.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/example_debug.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//thirdparty/some/library:library")
+          .setBuildFile(sourceRoot("/thirdparty/some/library/BUILD"))
+          .setKind("java_import")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("thirdparty/some/library.jar"))
+                                   .setRuntimeJar(genRoot("thirdparty/some/library.jar"))
+                                   .setSourceJar(genRoot("thirdparty/some/library.srcjar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    BlazeLibrary library = findLibrary(result.libraries, "library.jar");
+    assertNotNull(library);
+    assertNotNull(library.getLibraryArtifact().sourceJar);
+  }
+
+  /**
+   * Test a project with a java test rule
+   */
+  @Test
+  public void testJavaTestRule() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("javatests/com/google/android/apps/example"))))
+      .put(ListSection.builder(TestSourceSection.KEY).add(new Glob("javatests/*")))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/subdir/SubdirHelper.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/android/apps/example/example_debug.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/android/apps/example/example_debug.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//javatests/com/google/android/apps/example:example")
+          .setBuildFile(sourceRoot("javatests/com/google/android/apps/example/BUILD"))
+          .setKind("java_test")
+          .addSource(sourceRoot("javatests/com/google/android/apps/example/ExampleTests.java"))
+          .addDependency("//java/com/google/android/apps/example:example_debug")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("javatests/com/google/android/apps/example/example.jar"))
+                                   .setRuntimeJar(genRoot("javatests/com/google/android/apps/example/example.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.contentEntries).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google/android/apps/example")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/android/apps/example")
+                     .setPackagePrefix("com.google.android.apps.example")
+                     .build())
+        .build(),
+      BlazeContentEntry.builder("/root/javatests/com/google/android/apps/example")
+        .addSource(BlazeSourceDirectory.builder("/root/javatests/com/google/android/apps/example")
+                     .setPackagePrefix("com.google.android.apps.example")
+                     .setTest(true)
+                     .build())
+        .build()
+    );
+  }
+
+
+  /*
+   * Test that the non-android libraries can be imported.
+   */
+  @Test
+  public void testNormalJavaLibraryPackage() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/library/something"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/subdir/SubdirHelper.java"))
+          .setJavaInfo(JavaRuleIdeInfo.builder())
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .addDependency("//java/com/google/library/something:something")
+      )
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/library/something:something")
+          .setBuildFile(sourceRoot("java/com/google/library/something/BUILD"))
+          .setKind("java_library")
+          .addSource(sourceRoot("java/com/google/library/something/SomeJavaFile.java"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("java/com/google/library/something/something.jar"))
+                                   .setRuntimeJar(genRoot("java/com/google/library/something/something.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.contentEntries).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google/android/apps/example")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/android/apps/example")
+                     .setPackagePrefix("com.google.android.apps.example")
+                     .build())
+        .build(),
+      BlazeContentEntry.builder("/root/java/com/google/library/something")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/library/something")
+                     .setPackagePrefix("com.google.library.something")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testImportTargetOutputTag() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("lib")))
+             .add(DirectoryEntry.include(new WorkspacePath("lib2"))))
+      .build();
+
+    RuleMapBuilder response = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//lib:lib")
+          .setBuildFile(sourceRoot("lib/BUILD"))
+          .setKind("java_library")
+          .addDependency("//lib2:lib2")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("lib/lib.jar"))
+                                   .setRuntimeJar(genRoot("lib/lib.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//lib2:lib2")
+          .setBuildFile(sourceRoot("lib2/BUILD"))
+          .setKind("java_library")
+          .addTag("intellij-import-target-output")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("lib2/lib2.jar"))
+                                   .setRuntimeJar(genRoot("lib2/lib2.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      response,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+    assertEquals(1, result.libraries.size());
+  }
+
+  @Test
+  public void testImportAsLibraryTagLegacy() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("lib")))
+             .add(DirectoryEntry.include(new WorkspacePath("lib2"))))
+      .build();
+
+    RuleMapBuilder response = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//lib:lib")
+          .setBuildFile(sourceRoot("lib/BUILD"))
+          .setKind("java_library")
+          .addDependency("//lib2:lib2")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("lib/lib.jar"))
+                                   .setRuntimeJar(genRoot("lib/lib.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//lib2:lib2")
+          .setBuildFile(sourceRoot("lib2/BUILD"))
+          .setKind("java_library")
+          .addTag("aswb-import-as-library")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("lib2/lib2.jar"))
+                                   .setRuntimeJar(genRoot("lib2/lib2.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      response,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertEquals(1, result.libraries.size());
+  }
+
+  @Test
+  public void testMultipleImportOfJarsGetMerged() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("lib"))))
+      .build();
+
+    RuleMapBuilder response = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//lib:libsource")
+          .setBuildFile(sourceRoot("lib/BUILD"))
+          .setKind("java_library")
+          .setJavaInfo(JavaRuleIdeInfo.builder())
+          .addDependency("//lib:lib0")
+          .addDependency("//lib:lib1"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//lib:lib0")
+          .setBuildFile(sourceRoot("lib/BUILD"))
+          .setKind("java_import")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(sourceRoot("lib/lib.jar"))
+                                   .setRuntimeJar(sourceRoot("lib/lib.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//lib:lib1")
+          .setBuildFile(sourceRoot("lib/BUILD"))
+          .setKind("java_import")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(sourceRoot("lib/lib.jar"))
+                                   .setRuntimeJar(sourceRoot("lib/lib.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      response,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+    assertEquals(1, result.libraries.size()); // The libraries were merged
+  }
+
+  @Test
+  public void testRuleWithOnlyGeneratedSourcesIsAddedAsLibrary() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("import"))))
+      .build();
+
+    RuleMapBuilder response = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//import:lib")
+          .setBuildFile(sourceRoot("import/BUILD"))
+          .setKind("android_library")
+          .setJavaInfo(JavaRuleIdeInfo.builder())
+          .addDependency("//import:import")
+          .addDependency("//import:import_android"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//import:import")
+          .setBuildFile(sourceRoot("import/BUILD"))
+          .addSource(genRoot("import/GenSource.java"))
+          .setKind("java_library")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("import/import.jar"))
+                                   .setRuntimeJar(genRoot("import/import.jar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//import:import_android")
+          .setBuildFile(sourceRoot("import/BUILD"))
+          .addSource(genRoot("import/GenSource.java"))
+          .setKind("android_library")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("import/import_android.jar"))
+                                   .setRuntimeJar(genRoot("import/import_android.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      response,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(findLibrary(result.libraries, "import.jar")).isNotNull();
+    assertThat(findLibrary(result.libraries, "import_android.jar")).isNotNull();
+  }
+
+  @Test
+  public void testImportTargetOutput() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("import"))))
+      .put(ListSection.builder(ImportTargetOutputSection.KEY)
+             .add(new Label("//import:import")))
+      .build();
+
+    RuleMapBuilder response = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//import:lib")
+          .setBuildFile(sourceRoot("import/BUILD"))
+          .setKind("java_library")
+          .setJavaInfo(JavaRuleIdeInfo.builder())
+          .addDependency("//import:import"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//import:import")
+          .setBuildFile(sourceRoot("import/BUILD"))
+          .setKind("java_import")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("import/import.jar"))
+                                   .setRuntimeJar(genRoot("import/import.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      response,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.libraries).isNotEmpty();
+  }
+
+  private RuleMapBuilder ruleMapForJdepsSuite() {
+    return RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/Test.java"))
+          .setKind("java_library")
+          .setJavaInfo(JavaRuleIdeInfo.builder())
+          .addDependency("//thirdparty/a:a"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//thirdparty/a:a")
+          .setKind("java_library")
+          .setBuildFile(sourceRoot("third_party/a/BUILD"))
+          .addDependency("//thirdparty/b:b")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("thirdparty/a.jar"))
+                                   .setRuntimeJar(genRoot("thirdparty/a.jar"))
+                                   .setSourceJar(genRoot("thirdparty/a.srcjar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//thirdparty/b:b")
+          .setKind("java_library")
+          .setBuildFile(sourceRoot("third_party/b/BUILD"))
+          .addDependency("//thirdparty/c:c")
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("thirdparty/b.jar"))
+                                   .setRuntimeJar(genRoot("thirdparty/b.jar"))
+                                   .setSourceJar(genRoot("thirdparty/b.srcjar")))))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//thirdparty/c:c")
+          .setKind("java_library")
+          .setBuildFile(sourceRoot("third_party/c/BUILD"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+                         .addJar(LibraryArtifact.builder()
+                                   .setJar(genRoot("thirdparty/c.jar"))
+                                   .setRuntimeJar(genRoot("thirdparty/c.jar"))
+                                   .setSourceJar(genRoot("thirdparty/c.srcjar")))));
+  }
+
+  @Test
+  public void testLibraryDependenciesWithJdepsSet() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("javatests/com/google/android/apps/example"))))
+      .build();
+    RuleMapBuilder ruleMapBuilder = ruleMapForJdepsSuite();
+    jdepsMap.put(new Label("//java/com/google/android/apps/example:example_debug"), Lists.newArrayList(
+      jdepsPath("thirdparty/a.jar"),
+      jdepsPath("thirdparty/c.jar"))
+    );
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    assertThat(result.libraries.values().stream().map(BlazeJavaWorkspaceImporterTest::libraryFileName).collect(Collectors.toList()))
+      .containsExactly("a.jar", "c.jar");
+  }
+
+  @Test
+  public void testLibraryDependenciesWithJdepsReportingNothingShouldStillIncludeDirectDepsIfInWorkingSet() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("javatests/com/google/android/apps/example"))))
+      .build();
+    RuleMapBuilder ruleMapBuilder = ruleMapForJdepsSuite();
+    workingSet = new JavaWorkingSet(workspaceRoot, new WorkingSet(
+      ImmutableList.of(new WorkspacePath("java/com/google/android/apps/example/Test.java")),
+      ImmutableList.of(),
+      ImmutableList.of()
+    ));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    assertThat(result.libraries.values().stream().map(BlazeJavaWorkspaceImporterTest::libraryFileName).collect(Collectors.toList()))
+      .containsExactly("a.jar");
+  }
+
+  @Test
+  public void testLibraryDependenciesWithJdepsReportingNothingShouldNotIncludeDirectDepsIfNotInWorkingSet() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/android/apps/example")))
+             .add(DirectoryEntry.include(new WorkspacePath("javatests/com/google/android/apps/example"))))
+      .build();
+    RuleMapBuilder ruleMapBuilder = ruleMapForJdepsSuite();
+    workingSet = new JavaWorkingSet(workspaceRoot, new WorkingSet(
+      ImmutableList.of(),
+      ImmutableList.of(),
+      ImmutableList.of()
+    ));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    assertThat(result.libraries.values().stream().map(BlazeJavaWorkspaceImporterTest::libraryFileName).collect(Collectors.toList()))
+      .isEmpty();
+  }
+
+  /*
+   * Test the exclude_target section
+   */
+  @Test
+  public void testExcludeTarget() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY))
+      .put(ListSection.builder(ExcludeTargetSection.KEY)
+             .add(new Label("//java/com/google/android/apps/example:example_debug")))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/subdir/SubdirHelper.java"))
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+                            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+                            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+                            .setGenerateResourceClass(true)
+                            .setResourceJavaPackage("com.google.android.apps.example"))
+          .addDependency("//java/com/google/library/something:something")
+      );
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.libraries).isEmpty();
+  }
+
+  /**
+   * Test legacy proto_library jars, complete with overrides and everything.
+   */
+  @Test
+  public void testLegacyProtoLibraryInfo() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+             .add(DirectoryEntry.include(new WorkspacePath("java/com/google/example"))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/example:liba")
+          .setBuildFile(sourceRoot("java/com/google/example/BUILD"))
+          .setKind("java_library")
+          .setJavaInfo(JavaRuleIdeInfo.builder())
+          .addDependency("//thirdparty/proto/a:a"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/example:libb")
+          .setBuildFile(sourceRoot("java/com/google/example/BUILD"))
+          .setKind("java_library")
+          .setJavaInfo(JavaRuleIdeInfo.builder())
+          .addDependency("//thirdparty/proto/b:b"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//thirdparty/proto/a:a")
+          .setBuildFile(sourceRoot("/thirdparty/a/BUILD"))
+          .setKind("proto_library")
+          .setProtoLibraryLegacyInfo(ProtoLibraryLegacyInfo.builder(ProtoLibraryLegacyInfo.ApiFlavor.IMMUTABLE)
+                                       .addJarV1(LibraryArtifact.builder().setJar(genRoot("thirdparty/proto/a/liba-1-ijar.jar")))
+                                       .addJarImmutable(LibraryArtifact.builder().setJar(genRoot("thirdparty/proto/a/liba-ijar.jar"))))
+          .addDependency("//thirdparty/proto/b:b")
+          .addDependency("//thirdparty/proto/c:c"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//thirdparty/proto/b:b")
+          .setBuildFile(sourceRoot("/thirdparty/b/BUILD"))
+          .setKind("proto_library")
+          .setProtoLibraryLegacyInfo(ProtoLibraryLegacyInfo.builder(ProtoLibraryLegacyInfo.ApiFlavor.VERSION_1)
+                                       .addJarV1(LibraryArtifact.builder().setJar(genRoot("thirdparty/proto/b/libb-ijar.jar")))
+                                       .addJarImmutable(LibraryArtifact.builder().setJar(genRoot("thirdparty/proto/b/libb-2-ijar.jar"))))
+          .addDependency("//thirdparty/proto/d:d"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//thirdparty/proto/c:c")
+          .setBuildFile(sourceRoot("/thirdparty/c/BUILD"))
+          .setKind("proto_library")
+          .setProtoLibraryLegacyInfo(ProtoLibraryLegacyInfo.builder(ProtoLibraryLegacyInfo.ApiFlavor.IMMUTABLE)
+                                       .addJarV1(LibraryArtifact.builder().setJar(genRoot("thirdparty/proto/c/libc-1-ijar.jar")))
+                                       .addJarImmutable(LibraryArtifact.builder().setJar(genRoot("thirdparty/proto/c/libc-ijar.jar"))))
+          .addDependency("//thirdparty/proto/d:d"))
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//thirdparty/proto/d:d")
+          .setBuildFile(sourceRoot("/thirdparty/d/BUILD"))
+          .setKind("proto_library")
+          .setProtoLibraryLegacyInfo(ProtoLibraryLegacyInfo.builder(ProtoLibraryLegacyInfo.ApiFlavor.VERSION_1)
+                                       .addJarV1(LibraryArtifact.builder().setJar(genRoot("thirdparty/proto/d/libd-ijar.jar")))
+                                       .addJarImmutable(LibraryArtifact.builder().setJar(genRoot("thirdparty/proto/d/libd-2-ijar.jar")))));
+
+    workingSet = new JavaWorkingSet(workspaceRoot, new WorkingSet(ImmutableList.of(), ImmutableList.of(), ImmutableList.of()));
+
+    // First test - make sure that jdeps is working
+    jdepsMap.put(new Label("//java/com/google/example:liba"), Lists.newArrayList(jdepsPath("thirdparty/proto/a/liba-ijar.jar")));
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+    assertThat(result.libraries).hasSize(1);
+    assertThat(findLibrary(result.libraries, "liba-ijar.jar")).isNotNull();
+
+
+    // Second test - put everything in the working set, which should expand to the full transitive closure
+    workingSet = new JavaWorkingSet(workspaceRoot, new WorkingSet(
+      ImmutableList.of(new WorkspacePath("java/com/google/example/BUILD")),
+      ImmutableList.of(),
+      ImmutableList.of()
+    ));
+
+    result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.libraries).hasSize(6);
+    assertThat(findLibrary(result.libraries, "liba-ijar.jar")).isNotNull();
+    assertThat(findLibrary(result.libraries, "libb-ijar.jar")).isNotNull();
+    assertThat(findLibrary(result.libraries, "libb-2-ijar.jar")).isNotNull();
+    assertThat(findLibrary(result.libraries, "libc-ijar.jar")).isNotNull();
+    assertThat(findLibrary(result.libraries, "libd-ijar.jar")).isNotNull();
+    assertThat(findLibrary(result.libraries, "libd-2-ijar.jar")).isNotNull();
+  }
+
+  /*
+ * Test that the non-android libraries can be imported.
+ */
+  @Test
+  public void testImporterWorksWithWorkspaceRootDirectoryIncluded() {
+    ProjectView projectView = ProjectView.builder()
+      .put(ListSection.builder(DirectorySection.KEY)
+        .add(DirectoryEntry.include(new WorkspacePath(""))))
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/android/apps/example:example_debug")
+          .setBuildFile(sourceRoot("java/com/google/android/apps/example/BUILD"))
+          .setKind("android_binary")
+          .addSource(sourceRoot("java/com/google/android/apps/example/MainActivity.java"))
+          .addSource(sourceRoot("java/com/google/android/apps/example/subdir/SubdirHelper.java"))
+          .setJavaInfo(JavaRuleIdeInfo.builder())
+          .setAndroidInfo(AndroidRuleIdeInfo.builder()
+            .setManifestFile(sourceRoot("java/com/google/android/apps/example/AndroidManifest.xml"))
+            .addResource(sourceRoot("java/com/google/android/apps/example/res"))
+            .setGenerateResourceClass(true)
+            .setResourceJavaPackage("com.google.android.apps.example"))
+          .addDependency("//java/com/google/library/something:something")
+      )
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google/library/something:something")
+          .setBuildFile(sourceRoot("java/com/google/library/something/BUILD"))
+          .setKind("java_library")
+          .addSource(sourceRoot("java/com/google/library/something/SomeJavaFile.java"))
+          .setJavaInfo(JavaRuleIdeInfo.builder()
+            .addJar(LibraryArtifact.builder()
+              .setJar(genRoot("java/com/google/library/something/something.jar"))
+              .setRuntimeJar(genRoot("java/com/google/library/something/something.jar")))));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    errorCollector.assertNoIssues();
+
+    assertThat(result.contentEntries).containsExactly(
+      BlazeContentEntry.builder("/root")
+        .addSource(BlazeSourceDirectory.builder("/root")
+          .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testLanguageLevelIsReadFromToolchain() {
+    ProjectView projectView = ProjectView.builder()
+      .build();
+
+    RuleMapBuilder ruleMapBuilder = RuleMapBuilder.builder()
+      .addRule(
+        RuleIdeInfo.builder()
+          .setLabel("//java/com/google:toolchain")
+          .setBuildFile(sourceRoot("java/com/google/BUILD"))
+          .setKind("java_toolchain")
+          .setJavaToolchainIdeInfo(JavaToolchainIdeInfo.builder()
+                                     .setSourceVersion("8")
+                                     .setTargetVersion("8")));
+
+    BlazeJavaImportResult result = importWorkspace(
+      workspaceRoot,
+      ruleMapBuilder,
+      projectView
+    );
+    assertThat(result.sourceVersion).isEqualTo("8");
+  }
+
+  /* Utility methods */
+
+  private static String libraryFileName(BlazeLibrary library) {
+    return new File(library.getLibraryArtifact().jar.getRelativePath()).getName();
+  }
+
+  @Nullable
+  private static BlazeLibrary findLibrary(Map<LibraryKey, BlazeLibrary> libraries, String libraryName) {
+    for (BlazeLibrary library : libraries.values()) {
+      if (library.getLibraryArtifact().jar.getFile().getPath().endsWith(libraryName)) {
+        return library;
+      }
+    }
+    return null;
+  }
+
+  private ArtifactLocation sourceRoot(String relativePath) {
+    return ArtifactLocation.builder()
+      .setRootPath(FAKE_WORKSPACE_ROOT)
+      .setRelativePath(relativePath)
+      .setIsSource(true)
+      .build();
+  }
+
+  private static ArtifactLocation genRoot(String relativePath) {
+    return ArtifactLocation.builder()
+      .setRootPath(FAKE_GEN_ROOT)
+      .setRootExecutionPathFragment(FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT)
+      .setRelativePath(relativePath)
+      .setIsSource(false)
+      .build();
+  }
+
+  private static String jdepsPath(String relativePath) {
+    return FAKE_GEN_ROOT_EXECUTION_PATH_FRAGMENT + "/" + relativePath;
+  }
+}
diff --git a/blaze-java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java b/blaze-java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
new file mode 100644
index 0000000..2442f33
--- /dev/null
+++ b/blaze-java/tests/unittests/com/google/idea/blaze/java/sync/source/SourceDirectoryCalculatorTest.java
@@ -0,0 +1,1102 @@
+/*
+ * 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.source;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.idea.blaze.base.BlazeTestCase;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.executor.MockBlazeExecutor;
+import com.google.idea.blaze.base.experiments.ExperimentService;
+import com.google.idea.blaze.base.experiments.MockExperimentService;
+import com.google.idea.blaze.base.ideinfo.ArtifactLocation;
+import com.google.idea.blaze.base.io.FileAttributeProvider;
+import com.google.idea.blaze.base.io.InputStreamProvider;
+import com.google.idea.blaze.base.model.primitives.ExecutionRootPath;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspacePath;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+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.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolverImpl;
+import com.google.idea.blaze.java.sync.model.BlazeContentEntry;
+import com.google.idea.blaze.java.sync.model.BlazeSourceDirectory;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.JavaSourcePackage;
+import com.google.repackaged.devtools.build.lib.ideinfo.androidstudio.PackageManifestOuterClass.PackageManifest;
+import com.intellij.util.containers.HashMap;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+import java.io.*;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+
+/**
+ * Test cases for {@link SourceDirectoryCalculator}.
+ */
+public class SourceDirectoryCalculatorTest extends BlazeTestCase {
+
+  private static final ImmutableMap<Label, ArtifactLocation> NO_MANIFESTS = ImmutableMap.of();
+  private static final Label LABEL = new Label("//fake:label");
+
+  private MockInputStreamProvider mockInputStreamProvider;
+  private SourceDirectoryCalculator sourceDirectoryCalculator;
+
+  private BlazeContext context = new BlazeContext();
+  private ErrorCollector issues = new ErrorCollector();
+  private MockExperimentService experimentService;
+
+  private WorkspaceRoot workspaceRoot = new WorkspaceRoot(new File("/root"));
+  private ArtifactLocationDecoder decoder = new ArtifactLocationDecoder(
+    new BlazeRoots(
+      new File("/"),
+      Lists.newArrayList(new File("/usr/local/code")),
+      new ExecutionRootPath("out/crosstool/bin"),
+      new ExecutionRootPath("out/crosstool/gen")
+    ),
+    null
+  );
+
+  final static 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) {
+    super.initTest(applicationServices, projectServices);
+
+    mockInputStreamProvider = new MockInputStreamProvider();
+    applicationServices.register(
+      InputStreamProvider.class,
+      mockInputStreamProvider
+    );
+    applicationServices.register(JavaSourcePackageReader.class, new JavaSourcePackageReader());
+    applicationServices.register(PackageManifestReader.class, new PackageManifestReader());
+    applicationServices.register(FileAttributeProvider.class, new MockFileAttributeProvider());
+
+    context.addOutputSink(IssueOutput.class, issues);
+    sourceDirectoryCalculator = new SourceDirectoryCalculator();
+
+    BlazeExecutor blazeExecutor = new MockBlazeExecutor();
+    applicationServices.register(BlazeExecutor.class, blazeExecutor);
+
+    experimentService = new MockExperimentService();
+    applicationServices.register(ExperimentService.class, experimentService);
+
+    applicationServices.register(PrefetchService.class, new MockPrefetchService());
+  }
+
+  @Test
+  public void testWorkspacePathIsAddedWithoutSources() throws Exception {
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of();
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false /* isTest */),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google/app")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google/app")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/app")
+                     .setPackagePrefix("com.google.app")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testCalculatesPackageForSimpleCase() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.google;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build());
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+      .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                 .setPackagePrefix("com.google")
+                 .build())
+      .build()
+    );
+    issues.assertNoIssues();
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_testReturnsTest() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.google;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build());
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(true),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+      .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                 .setPackagePrefix("com.google")
+                 .setTest(true)
+                 .build())
+      .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_multipleMatchingPackagesAreMerged() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.google;\n public class Bla {}")
+      .addFile("/root/java/com/google/subpackage/Bla.java",
+               "package com.google.subpackage;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/subpackage/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+      .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                 .setPackagePrefix("com.google")
+                 .build())
+      .build()
+    );
+  }
+
+  @Test
+  public void testMultipleDirectoriesAreMergedWithDirectoryRootAsWorkspaceRoot() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/idea/blaze/plugin/run/Run.java",
+               "package com.google.idea.blaze.plugin.run;\n public class run {}")
+      .addFile("/root/java/com/google/idea/blaze/plugin/sync/Sync.java",
+               "package com.google.idea.blaze.plugin.sync;\n public class Sync {}")
+      .addFile("/root/java/com/google/idea/blaze/plugin/Plugin.java",
+               "package com.google.idea.blaze.plugin;\n public class Plugin {}")
+    ;
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/idea/blaze/plugin/run/Run.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/idea/blaze/plugin/sync/Sync.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/idea/blaze/plugin/Plugin.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root")
+        .addSource(BlazeSourceDirectory.builder("/root")
+                     .setPackagePrefix("")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java")
+                     .setPackagePrefix("")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testIncorrectPackageInMiddleOfTreeCausesMergePointHigherUp() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/idea/blaze/plugin/run/Run.java",
+               "package com.google.idea.blaze.plugin.run;\n public class run {}")
+      .addFile("/root/java/com/google/idea/blaze/plugin/sync/Sync.java",
+               "package com.google.idea.blaze.plugin.sync;\n public class Sync {}")
+      .addFile("/root/java/com/google/idea/blaze/Incorrect.java",
+               "package com.google.idea.blaze.incorrect;\n public class Incorrect {}")
+    ;
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/idea/blaze/plugin/run/Run.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/idea/blaze/plugin/sync/Sync.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/idea/blaze/Incorrect.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root")
+        .addSource(BlazeSourceDirectory.builder("/root")
+                     .setPackagePrefix("")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/idea/blaze")
+                     .setPackagePrefix("com.google.idea.blaze.incorrect")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/idea/blaze/plugin")
+                     .setPackagePrefix("com.google.idea.blaze.plugin")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_multipleNonMatchingPackagesAreNotMerged() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.google;\n public class Bla {}")
+      .addFile("/root/java/com/google/subpackage/Bla.java",
+               "package com.google.different;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/subpackage/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+      .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                 .setPackagePrefix("com.google")
+                 .build())
+      .addSource(BlazeSourceDirectory.builder("/root/java/com/google/subpackage")
+        .setPackagePrefix("com.google.different")
+                 .build())
+      .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_childMatchesPathButParentDoesnt() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.facebook;\n public class Bla {}")
+      .addFile("/root/java/com/google/subpackage/Bla.java",
+               "package com.google.subpackage;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/subpackage/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                     .setPackagePrefix("com.facebook")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/subpackage")
+                     .setPackagePrefix("com.google.subpackage")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_orderIsIrrelevant() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.google;\n public class Bla {}")
+      .addFile("/root/java/com/google/subpackage/Bla.java",
+               "package com.google.different;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/subpackage/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+      .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                   .setPackagePrefix("com.google")
+                 .build())
+      .addSource(BlazeSourceDirectory.builder("/root/java/com/google/subpackage")
+                   .setPackagePrefix("com.google.different")
+                   .build())
+      .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_packagesMatchPath() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.google;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build());
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                     .setPackagePrefix("com.google")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_packagesDoNotMatchPath() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.facebook;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build());
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                     .setPackagePrefix("com.facebook")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_completePackagePathMismatch() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/org/foo/Bla.java",
+               "package com.facebook;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/org/foo/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build());
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/org")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/org")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/org")
+                     .setPackagePrefix("com.org")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/org/foo")
+                     .setPackagePrefix("com.facebook")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_sourcesOutsideOfModuleGeneratesIssue() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/facebook/Bla.java",
+               "package com.facebook;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/facebook/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build());
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+
+    issues.assertIssueContaining("Did not add");
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_generatedSourcesOutsideOfModuleGeneratesNoIssue() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/facebook/Bla.java",
+               "package com.facebook;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/facebook/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(false))
+        .build());
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google/my")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_missingPackageDeclaration() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build());
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+
+    issues.assertIssueContaining("No package name string found");
+  }
+
+  @Test
+  public void testCompetingPackageDeclarationPicksMajority() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Foo.java",
+               "package com.google.different;\n public class Foo {}")
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.google;\n public class Bla {}")
+      .addFile("/root/java/com/google/Bla2.java",
+               "package com.google;\n public class Bla2 {}")
+      .addFile("/root/java/com/google/Bla3.java",
+               "package com.google;\n public class Bla3 {}")
+    ;
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla2.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla3.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Foo.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                     .setPackagePrefix("com.google")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_packagesMatchPathButNotAtRoot() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/Bla.java",
+               "package com.google.different;\n public class Bla {}")
+      .addFile("/root/java/com/google/subpackage/Bla.java",
+               "package com.google.subpackage;\n public class Bla {}")
+      .addFile("/root/java/com/google/subpackage/subsubpackage/Bla.java",
+               "package com.google.subpackage.subsubpackage;\n public class Bla {}")
+    ;
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/subpackage/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/subpackage/subsubpackage/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                     .setPackagePrefix("com.google.different")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/subpackage")
+                     .setPackagePrefix("com.google.subpackage")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testSourcesToSourceDirectories_multipleSubdirectoriesAreNotMerged() throws Exception {
+    mockInputStreamProvider
+      .addFile("/root/java/com/google/package0/Bla.java",
+               "package com.google.packagewrong0;\n public class Bla {}")
+      .addFile("/root/java/com/google/package1/Bla.java",
+               "package com.google.packagewrong1;\n public class Bla {}");
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/package0/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/package1/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build()
+    );
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      decoder,
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      NO_MANIFESTS
+    );
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                     .setPackagePrefix("com.google")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/package0")
+                     .setPackagePrefix("com.google.packagewrong0")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/package1")
+                     .setPackagePrefix("com.google.packagewrong1")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testOldFormatManifest() throws Exception {
+    setOldFormatPackageManifest(
+      "/root/java/com/test.manifest",
+      ImmutableList.of("/root/java/com/google/Bla.java"),
+      ImmutableList.of("com.google")
+    );
+    ImmutableMap<Label, ArtifactLocation> manifests = ImmutableMap.<Label, ArtifactLocation>builder()
+      .put(LABEL, ArtifactLocation.builder()
+        .setRelativePath("java/com/test.manifest")
+        .setRootPath("/root")
+        .setIsSource(true)
+        .build())
+      .build();
+    Map<Label, Map<String, String>> manifestMap = readPackageManifestFiles(manifests, getDecoder("/root"));
+
+    assertThat(manifestMap.get(LABEL)).containsEntry(
+      "/root/java/com/google/Bla.java",
+      "com.google");
+  }
+
+  @Test
+  public void testNewFormatManifest() throws Exception {
+    setNewFormatPackageManifest(
+      "/root/java/com/test.manifest",
+      ImmutableList.of(
+        PackageManifestOuterClass.ArtifactLocation.newBuilder()
+          .setRelativePath("java/com/google/Bla.java")
+          .setIsSource(true)
+          .build()),
+      ImmutableList.of("com.google")
+    );
+    ImmutableMap<Label, ArtifactLocation> manifests = ImmutableMap.<Label, ArtifactLocation>builder()
+      .put(LABEL, ArtifactLocation.builder()
+        .setRelativePath("java/com/test.manifest")
+        .setRootPath("/root")
+        .setIsSource(true)
+        .build())
+      .build();
+    Map<Label, Map<String, String>> manifestMap = readPackageManifestFiles(manifests, getDecoder("/root"));
+
+    assertThat(manifestMap.get(LABEL)).containsEntry(
+      "/root/java/com/google/Bla.java",
+      "com.google");
+  }
+
+  @Test
+  public void testManifestSingleFile() throws Exception {
+    setPackageManifest(
+      "/root",
+      "/root/java/com/test.manifest",
+      ImmutableList.of("java/com/google/Bla.java"),
+      ImmutableList.of("com.google")
+    );
+    ImmutableMap<Label, ArtifactLocation> manifests = ImmutableMap.<Label, ArtifactLocation>builder()
+      .put(LABEL, ArtifactLocation.builder()
+        .setRelativePath("java/com/test.manifest")
+        .setRootPath("/root")
+        .setIsSource(true)
+        .build())
+      .build();
+    Map<Label, Map<String, String>> manifestMap = readPackageManifestFiles(manifests, getDecoder("/root"));
+
+    assertThat(manifestMap.get(LABEL)).containsEntry(
+      "/root/java/com/google/Bla.java",
+      "com.google");
+  }
+
+  @Test
+  public void testManifestRepeatedSources() throws Exception {
+    setPackageManifest(
+      "/root",
+      "/root/java/com/test.manifest",
+      ImmutableList.of("java/com/google/Bla.java",
+                    "java/com/google/Foo.java"),
+      ImmutableList.of("com.google", "com.google.subpackage")
+    );
+    setPackageManifest(
+      "/root",
+      "/root/java/com/test2.manifest",
+      ImmutableList.of("java/com/google/Bla.java",
+                    "java/com/google/other/Temp.java"),
+      ImmutableList.of("com.google", "com.google.other")
+    );
+    ImmutableMap<Label, ArtifactLocation> manifests = ImmutableMap.<Label, ArtifactLocation>builder()
+      .put(new Label("//a:a"), ArtifactLocation.builder()
+        .setRelativePath("java/com/test.manifest")
+        .setRootPath("/root")
+        .setIsSource(true)
+        .build())
+      .put(new Label("//b:b"), ArtifactLocation.builder()
+        .setRelativePath("java/com/test2.manifest")
+        .setRootPath("/root")
+        .setIsSource(true)
+        .build())
+      .build();
+    Map<Label, Map<String, String>> manifestMap = readPackageManifestFiles(manifests, getDecoder("/root"));
+
+    assertThat(manifestMap).hasSize(2);
+
+    assertThat(manifestMap.get(new Label("//a:a"))).containsEntry(
+      "/root/java/com/google/Bla.java",
+      "com.google");
+    assertThat(manifestMap.get(new Label("//a:a"))).containsEntry(
+      "/root/java/com/google/Foo.java",
+      "com.google.subpackage");
+    assertThat(manifestMap.get(new Label("//b:b"))).containsEntry(
+      "/root/java/com/google/other/Temp.java",
+      "com.google.other");
+  }
+
+  @Test
+  public void testManifestMissingSourcesFallback() throws Exception {
+    setPackageManifest(
+      "/root",
+      "/root/java/com/test.manifest",
+      ImmutableList.of("java/com/google/Bla.java",
+                    "java/com/google/Foo.java"),
+      ImmutableList.of("com.google", "com.google")
+    );
+
+    mockInputStreamProvider.addFile("/root/java/com/google/subpackage/Bla.java",
+                                    "package com.google.different;\n public class Bla {}");
+
+    ImmutableMap<Label, ArtifactLocation> manifests = ImmutableMap.<Label, ArtifactLocation>builder()
+      .put(LABEL,
+           ArtifactLocation.builder()
+             .setRelativePath("java/com/test.manifest")
+             .setRootPath("/root")
+             .setIsSource(true)
+             .build())
+      .build();
+
+    List<SourceArtifact> sourceArtifacts = ImmutableList.of(
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/Foo.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build(),
+      SourceArtifact.builder(LABEL)
+        .setArtifactLocation(ArtifactLocation.builder()
+                               .setRelativePath("java/com/google/subpackage/Bla.java")
+                               .setRootPath("/root")
+                               .setIsSource(true))
+        .build());
+
+    ImmutableList<BlazeContentEntry> result = sourceDirectoryCalculator.calculateContentEntries(
+      context,
+      workspaceRoot,
+      new TestSourceImportConfig(false),
+      getDecoder("/root"),
+      ImmutableList.of(new WorkspacePath("java/com/google")),
+      sourceArtifacts,
+      manifests
+    );
+
+    issues.assertNoIssues();
+    assertThat(result).containsExactly(
+      BlazeContentEntry.builder("/root/java/com/google")
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google")
+                     .setPackagePrefix("com.google")
+                     .build())
+        .addSource(BlazeSourceDirectory.builder("/root/java/com/google/subpackage")
+                     .setPackagePrefix("com.google.different")
+                     .build())
+        .build()
+    );
+  }
+
+  @Test
+  public void testCandidateRootIterator() {
+    WorkspacePath directoryRoot = new WorkspacePath("com/google");
+    assertThat(new SourceDirectoryCalculator.CandidateRoots(directoryRoot, new SourceDirectoryCalculator.SourceRoot(
+      new WorkspacePath("com/google/a/b/c"), "com.google.a.b.c"))
+    ).containsExactly(
+      new SourceDirectoryCalculator.SourceRoot(new WorkspacePath("com/google/a/b"), "com.google.a.b"),
+      new SourceDirectoryCalculator.SourceRoot(new WorkspacePath("com/google/a"), "com.google.a"),
+      new SourceDirectoryCalculator.SourceRoot(new WorkspacePath("com/google"), "com.google")
+    ).inOrder();
+    assertThat(new SourceDirectoryCalculator.CandidateRoots(directoryRoot, new SourceDirectoryCalculator.SourceRoot(
+      new WorkspacePath("com/google/directory"), "com.google.different"))
+    ).isEmpty();
+  }
+
+  private void setPackageManifest(String rootPath,
+                                  String manifestPath,
+                                  List<String> sourceRelativePaths,
+                                  List<String> packages) {
+    PackageManifest.Builder manifest = PackageManifest.newBuilder();
+    for (int i = 0; i < sourceRelativePaths.size(); i++) {
+      String sourceRelativePath = sourceRelativePaths.get(i);
+      String absPath = Paths.get(rootPath, sourceRelativePath).toString();
+      PackageManifestOuterClass.ArtifactLocation source = PackageManifestOuterClass.ArtifactLocation.newBuilder()
+        .setRootPath(rootPath)
+        .setRelativePath(sourceRelativePath)
+        .setIsSource(true)
+        .build();
+      manifest.addSources(JavaSourcePackage.newBuilder()
+                            .setAbsolutePath(absPath)
+                            .setArtifactLocation(source)
+                            .setPackageString(packages.get(i)));
+    }
+    mockInputStreamProvider.addFile(manifestPath, manifest.build().toByteArray());
+  }
+
+  private void setOldFormatPackageManifest(String manifestPath, List<String> sourcePaths, List<String> packages) {
+    PackageManifest.Builder manifest = PackageManifest.newBuilder();
+    for (int i = 0; i < sourcePaths.size(); i++) {
+      manifest.addSources(JavaSourcePackage.newBuilder()
+                            .setAbsolutePath(sourcePaths.get(i))
+                            .setPackageString(packages.get(i)));
+    }
+    mockInputStreamProvider.addFile(manifestPath, manifest.build().toByteArray());
+  }
+
+  private void setNewFormatPackageManifest(String manifestPath,
+                                           List<PackageManifestOuterClass.ArtifactLocation> sources,
+                                           List<String> packages) {
+    PackageManifest.Builder manifest = PackageManifest.newBuilder();
+    for (int i = 0; i < sources.size(); i++) {
+      manifest.addSources(JavaSourcePackage.newBuilder()
+                            .setArtifactLocation(sources.get(i))
+                            .setPackageString(packages.get(i)));
+    }
+    mockInputStreamProvider.addFile(manifestPath, manifest.build().toByteArray());
+  }
+
+  private static ArtifactLocationDecoder getDecoder(String rootPath) {
+    File root = new File(rootPath);
+    WorkspaceRoot workspaceRoot = new WorkspaceRoot(root);
+    BlazeRoots roots = new BlazeRoots(
+      root,
+      ImmutableList.of(root),
+      new ExecutionRootPath("out/crosstool/bin"),
+      new ExecutionRootPath("out/crosstool/gen")
+    );
+    return new ArtifactLocationDecoder(roots, new WorkspacePathResolverImpl(workspaceRoot, roots));
+  }
+
+  private static class MockInputStreamProvider implements InputStreamProvider {
+
+    private final Map<String, InputStream> javaFiles = new HashMap<String, InputStream>();
+
+    public MockInputStreamProvider addFile(String filePath, String javaSrc) {
+      try {
+        addFile(filePath, javaSrc.getBytes("UTF-8"));
+      }
+      catch (UnsupportedEncodingException e) {
+        fail(e.getMessage());
+      }
+      return this;
+    }
+
+    public MockInputStreamProvider addFile(String filePath, byte[] contents) {
+      javaFiles.put(filePath, new ByteArrayInputStream(contents));
+      return this;
+    }
+
+    @Override
+    public InputStream getFile(@NotNull File path) throws FileNotFoundException {
+      final InputStream inputStream = javaFiles.get(path.getPath());
+      if (inputStream == null) {
+        throw new FileNotFoundException(
+          path + " has not been mapped into MockInputStreamProvider.");
+      }
+      return inputStream;
+    }
+  }
+
+  private Map<Label, Map<String, String>> readPackageManifestFiles(
+    Map<Label, ArtifactLocation> manifests,
+    ArtifactLocationDecoder decoder) {
+    return PackageManifestReader.getInstance().readPackageManifestFiles(context, decoder, manifests, MoreExecutors.sameThreadExecutor());
+  }
+
+  static class MockFileAttributeProvider extends FileAttributeProvider {
+    @Override
+    public long getFileModifiedTime(@NotNull File file) {
+      return 0;
+    }
+  }
+}
\ No newline at end of file
diff --git a/blaze-plugin-dev/BUILD b/blaze-plugin-dev/BUILD
new file mode 100644
index 0000000..0377443
--- /dev/null
+++ b/blaze-plugin-dev/BUILD
@@ -0,0 +1,42 @@
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "blaze-plugin-dev",
+    srcs = glob(["src/**/*.java"]),
+    deps = [
+        "//blaze-base",
+        "//blaze-java",
+        "//intellij-platform-sdk:devkit",
+        "//intellij-platform-sdk:plugin_api",
+        "//third_party:jsr305",
+    ],
+)
+
+filegroup(
+    name = "plugin_xml",
+    srcs = ["src/META-INF/blaze-plugin-dev.xml"],
+)
+
+load(
+    "//intellij_test:test_defs.bzl",
+    "intellij_test",
+)
+
+intellij_test(
+    name = "integration_tests",
+    srcs = glob(["tests/integrationtests/**/*.java"]),
+    integration_tests = True,
+    required_plugins = "com.google.idea.blaze.ijwb",
+    test_package_root = "com.google.idea.blaze.plugin",
+    deps = [
+        ":blaze-plugin-dev",
+        "//blaze-base",
+        "//blaze-base:integration_test_utils",
+        "//blaze-base:unit_test_utils",
+        "//ijwb:ijwb_bazel",
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//intellij_test:lib",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
diff --git a/blaze-plugin-dev/src/META-INF/blaze-plugin-dev.xml b/blaze-plugin-dev/src/META-INF/blaze-plugin-dev.xml
new file mode 100644
index 0000000..8725148
--- /dev/null
+++ b/blaze-plugin-dev/src/META-INF/blaze-plugin-dev.xml
@@ -0,0 +1,29 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+  <depends>DevKit</depends>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <RuleConfigurationFactory implementation="com.google.idea.blaze.plugin.run.BlazeIntellijPluginConfigurationType$BlazeIntellijPluginRuleConfigurationFactory"/>
+    <SyncPlugin implementation="com.google.idea.blaze.plugin.sync.IntellijPluginSyncPlugin"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <configurationType implementation="com.google.idea.blaze.plugin.run.BlazeIntellijPluginConfigurationType"/>
+    <stepsBeforeRunProvider implementation="com.google.idea.blaze.plugin.run.BuildPluginBeforeRunTaskProvider"/>
+  </extensions>
+
+</idea-plugin>
\ No newline at end of file
diff --git a/blaze-plugin-dev/src/com/google/idea/blaze/plugin/IntellijPluginRule.java b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/IntellijPluginRule.java
new file mode 100644
index 0000000..29f2271
--- /dev/null
+++ b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/IntellijPluginRule.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin;
+
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Kind;
+
+/**
+ * Utility methods for intellij_plugin blaze rules
+ */
+public class IntellijPluginRule {
+
+  public static final String RULE_TAG_IJ_PLUGIN = "intellij-plugin";
+
+  public static boolean isPluginRule(RuleIdeInfo rule) {
+    return rule.kindIsOneOf(Kind.JAVA_IMPORT) && rule.tags.contains(RULE_TAG_IJ_PLUGIN);
+  }
+}
diff --git a/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
new file mode 100644
index 0000000..eff1164
--- /dev/null
+++ b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfiguration.java
@@ -0,0 +1,466 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin.run;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.JavaRuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.TargetExpression;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.run.BlazeRunConfiguration;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.ui.UiUtil;
+import com.google.idea.blaze.plugin.IntellijPluginRule;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.Executor;
+import com.intellij.execution.configurations.*;
+import com.intellij.execution.process.OSProcessHandler;
+import com.intellij.execution.process.ProcessAdapter;
+import com.intellij.execution.process.ProcessEvent;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManagerCore;
+import com.intellij.openapi.application.JetBrainsProtocolHandler;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.options.ConfigurationException;
+import com.intellij.openapi.options.SettingsEditor;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.projectRoots.ProjectJdkTable;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.roots.ProjectRootManager;
+import com.intellij.openapi.roots.ui.configuration.JdkComboBox;
+import com.intellij.openapi.roots.ui.configuration.projectRoot.ProjectSdksModel;
+import com.intellij.openapi.ui.ComboBox;
+import com.intellij.openapi.ui.LabeledComponent;
+import com.intellij.openapi.util.BuildNumber;
+import com.intellij.openapi.util.InvalidDataException;
+import com.intellij.openapi.util.WriteExternalException;
+import com.intellij.ui.ListCellRendererWrapper;
+import com.intellij.ui.RawCommandLineEditor;
+import com.intellij.util.PlatformUtils;
+import com.intellij.util.execution.ParametersListUtil;
+import org.jdom.Element;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.awt.*;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * A run configuration that builds a plugin jar via blaze, copies it to the
+ * SDK sandbox, then runs IJ with the plugin loaded.
+ */
+public class BlazeIntellijPluginConfiguration extends LocatableConfigurationBase implements BlazeRunConfiguration, ModuleRunConfiguration {
+
+  private static final String TARGET_TAG = "blaze-target";
+  private static final String USER_BLAZE_FLAG_TAG = "blaze-user-flag";
+  private static final String USER_EXE_FLAG_TAG = "blaze-user-exe-flag";
+  private static final String SDK_ATTR = "blaze-plugin-sdk";
+  private static final String VM_PARAMS_ATTR = "blaze-vm-params";
+  private static final String PROGRAM_PARAMS_ATTR = "blaze-program-params";
+
+  private final String buildSystem;
+
+  @Nullable private Label target;
+  private ImmutableList<String> blazeFlags = ImmutableList.of();
+  private ImmutableList<String> exeFlags = ImmutableList.of();
+  @Nullable private Sdk pluginSdk;
+  @Nullable private String vmParameters;
+  @Nullable private String programParameters;
+
+  public BlazeIntellijPluginConfiguration(
+    Project project,
+    ConfigurationFactory factory,
+    String name,
+    @Nullable RuleIdeInfo initialRule) {
+    super(project, factory, name);
+    this.buildSystem = Blaze.buildSystemName(project);
+    if (initialRule != null) {
+      target = initialRule.label;
+    }
+    Sdk projectSdk = ProjectRootManager.getInstance(project).getProjectSdk();
+    if (IdeaJdkHelper.isIdeaJdk(projectSdk)) {
+      pluginSdk = projectSdk;
+    }
+  }
+
+  @Override
+  @Nullable
+  public Label getTarget() {
+    return target;
+  }
+
+  public void setTarget(Label target) {
+    this.target = target;
+  }
+
+  private File findPluginJar() throws ExecutionException {
+    RuleIdeInfo rule = RuleFinder.getInstance().ruleForTarget(getProject(), getTarget());
+    if (rule == null) {
+      throw new ExecutionException(buildSystem + " rule '" + getTarget() + "' not imported during sync");
+    }
+    JavaRuleIdeInfo javaRuleIdeInfo = rule.javaRuleIdeInfo;
+    if (javaRuleIdeInfo == null) {
+      throw new ExecutionException(buildSystem + " rule '" + getTarget() + "' is not a valid intellij_plugin rule");
+    }
+    Collection<LibraryArtifact> jars = javaRuleIdeInfo.jars;
+    if (javaRuleIdeInfo.jars.size() > 1) {
+      throw new ExecutionException("Invalid IntelliJ plugin rule: it has multiple output jars");
+    }
+    LibraryArtifact artifact = jars.isEmpty() ? null : jars.iterator().next();
+    if (artifact == null || artifact.runtimeJar == null) {
+      throw new ExecutionException("No output plugin jar found for '" + getTarget() + "'");
+    }
+    return artifact.runtimeJar.getFile();
+  }
+
+  /**
+   * Plugin jar has been previously created via blaze build. This method:
+   *  - copies jar to sandbox environment
+   *  - cracks open jar and finds plugin.xml (with ID, etc., needed for JVM args)
+   *  - sets up the SDK, etc. (use project SDK?)
+   *  - sets up the JVM, and returns a JavaCommandLineState
+   */
+  @Nullable
+  @Override
+  public RunProfileState getState(Executor executor, ExecutionEnvironment env) throws ExecutionException {
+    final Sdk ideaJdk = pluginSdk;
+    if (!IdeaJdkHelper.isIdeaJdk(ideaJdk)) {
+      throw new ExecutionException("Choose an IntelliJ Platform Plugin SDK");
+    }
+    String sandboxHome = IdeaJdkHelper.getSandboxHome(ideaJdk);
+    if (sandboxHome == null){
+      throw new ExecutionException("No sandbox specified for IntelliJ Platform Plugin SDK");
+    }
+
+    try {
+      sandboxHome = new File(sandboxHome).getCanonicalPath();
+    }
+    catch (IOException e) {
+      throw new ExecutionException("No sandbox specified for IntelliJ Platform Plugin SDK");
+    }
+    final String canonicalSandbox = sandboxHome;
+    final File pluginJar = findPluginJar();
+    if (!pluginJar.exists()) {
+      throw new ExecutionException("No plugin jar found. Did the " + buildSystem + " build fail?");
+    }
+    final File pluginJarDestination = new File(canonicalSandbox, "plugins/" + pluginJar.getName());
+
+    // copy license from running instance of idea
+    IdeaJdkHelper.copyIDEALicense(sandboxHome);
+
+    final JavaCommandLineState state = new JavaCommandLineState(env) {
+      @Override
+      protected JavaParameters createJavaParameters() throws ExecutionException {
+
+        // copy plugin jar to sandbox
+        IdeaPluginDescriptor pluginDescriptor = PluginManagerCore.loadDescriptor(pluginJar, "plugin.xml");
+        String buildNumber = IdeaJdkHelper.getBuildNumber(ideaJdk);
+        if (PluginManagerCore.isIncompatible(pluginDescriptor, BuildNumber.fromString(buildNumber))) {
+          throw new ExecutionException(
+            String.format("Plugin SDK version '%s' is incompatible with this plugin (since: '%s', until: '%s')",
+                          buildNumber, pluginDescriptor.getSinceBuild(), pluginDescriptor.getUntilBuild()));
+        }
+
+        try {
+          pluginJarDestination.getParentFile().mkdirs();
+          Files.copy(pluginJar.toPath(), pluginJarDestination.toPath(), StandardCopyOption.REPLACE_EXISTING);
+        }
+        catch (IOException e) {
+          throw new ExecutionException("Error copying plugin jar to sandbox", e);
+        }
+
+        final JavaParameters params = new JavaParameters();
+
+        ParametersList vm = params.getVMParametersList();
+
+        fillParameterList(vm, vmParameters);
+        fillParameterList(params.getProgramParametersList(), programParameters);
+
+        IntellijWithPluginClasspathHelper.addRequiredVmParams(params, ideaJdk);
+
+        vm.defineProperty(JetBrainsProtocolHandler.REQUIRED_PLUGINS_KEY, pluginDescriptor.getPluginId().toString());
+
+        if (!vm.hasProperty(PlatformUtils.PLATFORM_PREFIX_KEY) && buildNumber != null) {
+          String prefix = IdeaJdkHelper.getPlatformPrefix(buildNumber);
+          if (prefix != null) {
+            vm.defineProperty(PlatformUtils.PLATFORM_PREFIX_KEY, prefix);
+          }
+        }
+        return params;
+      }
+
+      @Override
+      protected OSProcessHandler startProcess() throws ExecutionException {
+        final OSProcessHandler handler = super.startProcess();
+        handler.addProcessListener(new ProcessAdapter() {
+          @Override
+          public void processTerminated(ProcessEvent event) {
+            pluginJarDestination.delete();
+          }
+        });
+        return handler;
+      }
+    };
+    return state;
+  }
+
+  private static void fillParameterList(ParametersList list, @Nullable String value) {
+    if (value == null) return;
+
+    for (String parameter : value.split(" ")) {
+      if (parameter != null && parameter.length() > 0) {
+        list.add(parameter);
+      }
+    }
+  }
+
+  @Override
+  public Module[] getModules() {
+    return Module.EMPTY_ARRAY;
+  }
+
+  @Override
+  public void checkConfiguration() throws RuntimeConfigurationException {
+    super.checkConfiguration();
+
+    Label target = getTarget();
+    if (target == null) {
+      throw new RuntimeConfigurationError("Select a target to run");
+    }
+    RuleIdeInfo rule = RuleFinder.getInstance().ruleForTarget(getProject(), target);
+    if (rule == null) {
+      throw new RuntimeConfigurationError("The selected target does not exist.");
+    }
+    if (!IntellijPluginRule.isPluginRule(rule)) {
+      throw new RuntimeConfigurationError(
+        "The selected target is not an intellij_plugin");
+    }
+    if (!IdeaJdkHelper.isIdeaJdk(pluginSdk)) {
+      throw new RuntimeConfigurationError("Select an IntelliJ Platform Plugin SDK");
+    }
+  }
+
+  @Override
+  public void readExternal(Element element) throws InvalidDataException {
+    super.readExternal(element);
+    // Target is persisted as a tag to permit multiple targets in the future.
+    Element targetElement = element.getChild(TARGET_TAG);
+    if (targetElement != null && !Strings.isNullOrEmpty(targetElement.getTextTrim())) {
+      target = (Label) TargetExpression.fromString(targetElement.getTextTrim());
+    }
+    else {
+      target = null;
+    }
+    blazeFlags = loadUserFlags(element, USER_BLAZE_FLAG_TAG);
+    exeFlags = loadUserFlags(element, USER_EXE_FLAG_TAG);
+
+    String sdkName = element.getAttributeValue(SDK_ATTR);
+    if (!Strings.isNullOrEmpty(sdkName)) {
+      pluginSdk = ProjectJdkTable.getInstance().findJdk(sdkName);
+    }
+    vmParameters = Strings.emptyToNull(element.getAttributeValue(VM_PARAMS_ATTR));
+    programParameters = Strings.emptyToNull(element.getAttributeValue(PROGRAM_PARAMS_ATTR));
+  }
+
+  private static ImmutableList<String> loadUserFlags(Element root,String tag) {
+    ImmutableList.Builder<String> flagsBuilder = ImmutableList.builder();
+    for (Element e : root.getChildren(tag)) {
+      String flag = e.getTextTrim();
+      if (flag != null && !flag.isEmpty()) {
+        flagsBuilder.add(flag);
+      }
+    }
+    return flagsBuilder.build();
+  }
+
+  private static void saveUserFlags(Element root, List<String> flags, String tag) {
+    for (String flag : flags) {
+      Element child = new Element(tag);
+      child.setText(flag);
+      root.addContent(child);
+    }
+  }
+
+  @Override
+  public void writeExternal(Element element) throws WriteExternalException {
+    super.writeExternal(element);
+    if (target != null) {
+      // Target is persisted as a tag to permit multiple targets in the future.
+      Element targetElement = new Element(TARGET_TAG);
+      targetElement.setText(target.toString());
+      element.addContent(targetElement);
+    }
+    saveUserFlags(element, blazeFlags, USER_BLAZE_FLAG_TAG);
+    saveUserFlags(element, exeFlags, USER_EXE_FLAG_TAG);
+    if (pluginSdk != null) {
+      element.setAttribute(SDK_ATTR, pluginSdk.getName());
+    }
+    if (vmParameters != null) {
+      element.setAttribute(VM_PARAMS_ATTR, vmParameters);
+    }
+    if (programParameters != null) {
+      element.setAttribute(PROGRAM_PARAMS_ATTR, programParameters);
+    }
+  }
+
+  @Override
+  public BlazeIntellijPluginConfiguration clone() {
+    final BlazeIntellijPluginConfiguration configuration = (BlazeIntellijPluginConfiguration) super.clone();
+    configuration.target = target;
+    configuration.blazeFlags = blazeFlags;
+    configuration.exeFlags = exeFlags;
+    configuration.pluginSdk = pluginSdk;
+    configuration.vmParameters = vmParameters;
+    configuration.programParameters = programParameters;
+    return configuration;
+  }
+
+  protected BlazeCommand buildBlazeCommand(Project project, ProjectViewSet projectViewSet) {
+    BlazeCommand.Builder command = BlazeCommand.builder(Blaze.getBuildSystem(getProject()), BlazeCommandName.BUILD)
+      .addTargets(getTarget())
+      .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+      .addBlazeFlags(blazeFlags)
+      .addExeFlags(exeFlags)
+      ;
+    return command.build();
+  }
+
+  @Override
+  public BlazeIntellijPluginConfigurationSettingsEditor getConfigurationEditor() {
+    List<RuleIdeInfo> javaRules = RuleFinder.getInstance().findRules(getProject(), IntellijPluginRule::isPluginRule);
+    List<Label> javaLabels = Lists.newArrayList();
+    for (RuleIdeInfo rule : javaRules) {
+      javaLabels.add(rule.label);
+    }
+    return new BlazeIntellijPluginConfigurationSettingsEditor(buildSystem, javaLabels);
+  }
+
+  @Override
+  @Nullable
+  public String suggestedName() {
+    Label target = getTarget();
+    if (target == null) {
+      return null;
+    }
+    return target.ruleName().toString();
+  }
+
+  @VisibleForTesting
+  static class BlazeIntellijPluginConfigurationSettingsEditor extends SettingsEditor<BlazeIntellijPluginConfiguration> {
+    private final String buildSystemName;
+    private final ComboBox targetCombo;
+    private final JTextArea blazeFlagsField = new JTextArea(5, 0);
+    private final JTextArea exeFlagsField = new JTextArea(5, 0);
+    private final JdkComboBox sdkCombo;
+    private final LabeledComponent<RawCommandLineEditor> vmParameters = new LabeledComponent<>();
+    private final LabeledComponent<RawCommandLineEditor> programParameters = new LabeledComponent<>();
+
+    public BlazeIntellijPluginConfigurationSettingsEditor(String buildSystemName, List<Label> javaLabels) {
+      this.buildSystemName = buildSystemName;
+      targetCombo = new ComboBox(new DefaultComboBoxModel(Ordering.usingToString().sortedCopy(javaLabels).toArray()));
+      targetCombo.setRenderer(new ListCellRendererWrapper<Label>() {
+        @Override
+        public void customize(JList list, @Nullable Label value, int index, boolean selected, boolean hasFocus) {
+          setText(value == null ? null : value.toString());
+        }
+      });
+
+      ProjectSdksModel sdksModel = new ProjectSdksModel();
+      sdksModel.reset(null);
+      sdkCombo = new JdkComboBox(sdksModel, IdeaJdkHelper::isIdeaJdkType);
+    }
+
+    @VisibleForTesting
+    @Override
+    public void resetEditorFrom(BlazeIntellijPluginConfiguration s) {
+      targetCombo.setSelectedItem(s.getTarget());
+      blazeFlagsField.setText(ParametersListUtil.join(s.blazeFlags));
+      exeFlagsField.setText(ParametersListUtil.join(s.exeFlags));
+      if (s.pluginSdk != null) {
+        sdkCombo.setSelectedJdk(s.pluginSdk);
+      } else {
+        s.pluginSdk = sdkCombo.getSelectedJdk();
+      }
+      if (s.vmParameters != null) {
+        vmParameters.getComponent().setText(s.vmParameters);
+      }
+      if (s.programParameters != null) {
+        programParameters.getComponent().setText(s.programParameters);
+      }
+    }
+
+    @VisibleForTesting
+    @Override
+    public void applyEditorTo(BlazeIntellijPluginConfiguration s) throws ConfigurationException {
+      try {
+        s.target = (Label)targetCombo.getSelectedItem();
+      }
+      catch (ClassCastException e) {
+        throw new ConfigurationException("Invalid label specified.");
+      }
+      s.blazeFlags = ImmutableList.copyOf(ParametersListUtil.parse(Strings.nullToEmpty(blazeFlagsField.getText())));
+      s.exeFlags = ImmutableList.copyOf(ParametersListUtil.parse(Strings.nullToEmpty(exeFlagsField.getText())));
+      s.pluginSdk = sdkCombo.getSelectedJdk();
+      s.vmParameters = vmParameters.getComponent().getText();
+      s.programParameters = programParameters.getComponent().getText();
+    }
+
+    @Override
+    protected JComponent createEditor() {
+      vmParameters.setText("VM options:");
+      vmParameters.setComponent(new RawCommandLineEditor());
+      vmParameters.getComponent().setDialogCaption(vmParameters.getRawText());
+      vmParameters.setLabelLocation(BorderLayout.WEST);
+
+      programParameters.setText("Program arguments");
+      programParameters.setComponent(new RawCommandLineEditor());
+      programParameters.getComponent().setDialogCaption(programParameters.getRawText());
+      programParameters.setLabelLocation(BorderLayout.WEST);
+
+      return UiUtil.createBox(
+        new JLabel("Target:"),
+        targetCombo,
+        new JLabel("Plugin SDK"),
+        sdkCombo,
+        vmParameters.getLabel(),
+        vmParameters.getComponent(),
+        programParameters.getLabel(),
+        programParameters.getComponent(),
+        new JLabel(buildSystemName + " flags:"),
+        blazeFlagsField,
+        new JLabel("Executable flags:"),
+        exeFlagsField
+      );
+    }
+  }
+}
+
diff --git a/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfigurationType.java b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfigurationType.java
new file mode 100644
index 0000000..0b8356f
--- /dev/null
+++ b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BlazeIntellijPluginConfigurationType.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin.run;
+
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.run.BlazeRuleConfigurationFactory;
+import com.google.idea.blaze.base.run.rulefinder.RuleFinder;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.plugin.IntellijPluginRule;
+import com.intellij.execution.BeforeRunTask;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.RunnerAndConfigurationSettings;
+import com.intellij.execution.configurations.ConfigurationFactory;
+import com.intellij.execution.configurations.ConfigurationType;
+import com.intellij.execution.configurations.ConfigurationTypeUtil;
+import com.intellij.icons.AllIcons;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+
+import javax.swing.*;
+
+/**
+ * A type for run configurations that build an IntelliJ plugin jar via blaze,
+ * then load them in an IntelliJ application
+ */
+public class BlazeIntellijPluginConfigurationType implements ConfigurationType {
+
+  private final BlazeIntellijPluginConfigurationFactory factory = new BlazeIntellijPluginConfigurationFactory(this);
+
+  public static class BlazeIntellijPluginRuleConfigurationFactory implements BlazeRuleConfigurationFactory {
+    @Override
+    public boolean handlesRule(WorkspaceLanguageSettings workspaceLanguageSettings, RuleIdeInfo rule) {
+      return workspaceLanguageSettings.isWorkspaceType(WorkspaceType.INTELLIJ_PLUGIN)
+             && IntellijPluginRule.isPluginRule(rule);
+    }
+
+    @Override
+    public RunnerAndConfigurationSettings createForRule(RunManager runManager, RuleIdeInfo rule) {
+      return getInstance().factory.createForRule(runManager, rule);
+    }
+  }
+
+  public static class BlazeIntellijPluginConfigurationFactory extends ConfigurationFactory {
+
+    protected BlazeIntellijPluginConfigurationFactory(ConfigurationType type) {
+      super(type);
+    }
+
+    @Override
+    public boolean isApplicable(Project project) {
+      return Blaze.isBlazeProject(project);
+    }
+
+    @Override
+    public BlazeIntellijPluginConfiguration createTemplateConfiguration(Project project) {
+      return new BlazeIntellijPluginConfiguration(
+        project,
+        this,
+        "Unnamed",
+        RuleFinder.getInstance().findFirstRule(project, IntellijPluginRule::isPluginRule)
+      );
+    }
+
+    @Override
+    public void configureBeforeRunTaskDefaults(
+      Key<? extends BeforeRunTask> providerID, BeforeRunTask task) {
+      task.setEnabled(providerID.equals(BuildPluginBeforeRunTaskProvider.ID));
+    }
+
+    public RunnerAndConfigurationSettings createForRule(RunManager runManager, RuleIdeInfo rule) {
+      final RunnerAndConfigurationSettings settings =
+        runManager.createRunConfiguration(rule.label.toString(), this);
+      final BlazeIntellijPluginConfiguration configuration =
+        (BlazeIntellijPluginConfiguration) settings.getConfiguration();
+      configuration.setTarget(rule.label);
+      return settings;
+    }
+
+    @Override
+    public boolean isConfigurationSingletonByDefault() {
+      return true;
+    }
+  }
+
+  public static BlazeIntellijPluginConfigurationType getInstance() {
+    return ConfigurationTypeUtil.findConfigurationType(BlazeIntellijPluginConfigurationType.class);
+  }
+
+  @Override
+  public String getDisplayName() {
+    return Blaze.defaultBuildSystemName() + " IntelliJ Plugin";
+  }
+
+  @Override
+  public String getConfigurationTypeDescription() {
+    return "Configuration for launching an IntelliJ plugin in a sandbox environment.";
+  }
+
+  @Override
+  public Icon getIcon() {
+    return AllIcons.Nodes.Plugin;
+  }
+
+  @Override
+  public String getId() {
+    return "BlazeIntellijPluginConfigurationType";
+  }
+
+  @Override
+  public BlazeIntellijPluginConfigurationFactory[] getConfigurationFactories() {
+    return new BlazeIntellijPluginConfigurationFactory[] {factory};
+  }
+
+  public BlazeIntellijPluginConfigurationFactory getFactory() {
+    return factory;
+  }
+
+}
diff --git a/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java
new file mode 100644
index 0000000..216621c
--- /dev/null
+++ b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/BuildPluginBeforeRunTaskProvider.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin.run;
+
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.idea.blaze.base.async.executor.BlazeExecutor;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.experiments.ExperimentScope;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.metrics.Action;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewManager;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.ScopedTask;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.scopes.BlazeConsoleScope;
+import com.google.idea.blaze.base.scope.scopes.IssuesScope;
+import com.google.idea.blaze.base.scope.scopes.LoggedTimingScope;
+import com.google.idea.blaze.base.settings.Blaze;
+import com.google.idea.blaze.base.settings.BlazeUserSettings;
+import com.google.idea.blaze.base.util.SaveUtil;
+import com.intellij.execution.BeforeRunTask;
+import com.intellij.execution.BeforeRunTaskProvider;
+import com.intellij.execution.configurations.RunConfiguration;
+import com.intellij.execution.runners.ExecutionEnvironment;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.Key;
+import com.intellij.openapi.vfs.LocalFileSystem;
+import icons.BlazeIcons;
+
+import javax.annotation.Nullable;
+import javax.swing.*;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * Builds the intellij_plugin jar via 'blaze build', for Blaze Intellij Plugin run configurations
+ */
+public final class BuildPluginBeforeRunTaskProvider extends BeforeRunTaskProvider<BuildPluginBeforeRunTaskProvider.Task> {
+  public static final Key<Task> ID = Key.create("Blaze.Intellij.Plugin.BeforeRunTask");
+
+  public static class Task extends BeforeRunTask<Task> {
+    private Task() {
+      super(ID);
+      setEnabled(true);
+    }
+  }
+
+  private final Project project;
+
+  public BuildPluginBeforeRunTaskProvider(Project project) {
+    this.project = project;
+  }
+
+  @Override
+  public Icon getIcon() {
+    return BlazeIcons.Blaze;
+  }
+
+  @Override
+  public Icon getTaskIcon(Task task) {
+    return BlazeIcons.Blaze;
+  }
+
+  @Override
+  public boolean isConfigurable() {
+    return false;
+  }
+
+  @Override
+  public boolean configureTask(RunConfiguration runConfiguration, Task task) {
+    return false;
+  }
+
+  @Override
+  public Key<Task> getId() {
+    return ID;
+  }
+
+  @Override
+  public String getName() {
+    return taskName();
+  }
+
+  @Override
+  public String getDescription(Task task) {
+    return taskName();
+  }
+
+  private String taskName() {
+    return Blaze.buildSystemName(project) + " build plugin before-run task";
+  }
+
+  @Override
+  public final boolean canExecuteTask(RunConfiguration configuration, Task task) {
+    return isValidConfiguration(configuration);
+  }
+
+  @Nullable
+  @Override
+  public Task createTask(RunConfiguration runConfiguration) {
+    if (isValidConfiguration(runConfiguration)) {
+      return new Task();
+    }
+    return null;
+  }
+
+  private static boolean isValidConfiguration(RunConfiguration runConfiguration) {
+    return runConfiguration instanceof BlazeIntellijPluginConfiguration;
+  }
+
+  @Override
+  public final boolean executeTask(
+    final DataContext dataContext,
+    final RunConfiguration configuration,
+    final ExecutionEnvironment env,
+    Task task) {
+    if (!canExecuteTask(configuration, task)) {
+      return false;
+    }
+    boolean suppressConsole = BlazeUserSettings.getInstance().getSuppressConsoleForRunAction();
+    return Scope.root(context -> {
+      context
+        .push(new ExperimentScope())
+        .push(new IssuesScope(project))
+        .push(new BlazeConsoleScope.Builder(project).setSuppressConsole(suppressConsole).build())
+      ;
+
+      final ProjectViewSet projectViewSet = ProjectViewManager.getInstance(project).getProjectViewSet();
+      if (projectViewSet == null) {
+        IssueOutput.error("Could not load project view. Please resync project").submit(context);
+        return false;
+      }
+
+      final ScopedTask buildTask = new ScopedTask(context) {
+        @Override
+        protected void execute(BlazeContext context) {
+          BlazeIntellijPluginConfiguration config = (BlazeIntellijPluginConfiguration) configuration;
+          BlazeCommand command = config.buildBlazeCommand(project, projectViewSet);
+          if (command == null || context.hasErrors() || context.isCancelled()) {
+            return;
+          }
+          SaveUtil.saveAllFiles();
+          WorkspaceRoot workspaceRoot = WorkspaceRoot.fromProject(project);
+          int retVal = ExternalTask.builder(workspaceRoot, command)
+            .context(context)
+            .stderr(LineProcessingOutputStream.of(new IssueOutputLineProcessor(project, context, workspaceRoot)))
+            .build()
+            .run(new LoggedTimingScope(project, Action.BLAZE_BUILD));
+          if (retVal != 0) {
+            context.setHasError();
+          }
+          LocalFileSystem.getInstance().refresh(true);
+        }
+      };
+
+      ListenableFuture<Void> buildFuture = BlazeExecutor.submitTask(
+        project,
+        "Executing blaze build for IntelliJ plugin jar",
+        buildTask
+      );
+
+      try {
+        Futures.get(buildFuture, ExecutionException.class);
+      }
+      catch (ExecutionException e) {
+        context.setHasError();
+      }
+      catch (CancellationException e) {
+        context.setCancelled();
+      }
+
+      if (context.hasErrors() || context.isCancelled()) {
+        return false;
+      }
+      return true;
+    });
+  }
+
+}
diff --git a/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/IdeaJdkHelper.java b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/IdeaJdkHelper.java
new file mode 100644
index 0000000..3b9b152
--- /dev/null
+++ b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/IdeaJdkHelper.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.plugin.run;
+
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.projectRoots.SdkTypeId;
+import org.jetbrains.idea.devkit.projectRoots.IdeaJdk;
+import org.jetbrains.idea.devkit.projectRoots.IntelliJPlatformProduct;
+import org.jetbrains.idea.devkit.projectRoots.Sandbox;
+import org.jetbrains.idea.devkit.run.IdeaLicenseHelper;
+
+import javax.annotation.Nullable;
+
+/**
+ * Contains all dependencies on devkit (not included in plugin SDK)
+ */
+public class IdeaJdkHelper {
+
+  public static boolean isIdeaJdk(@Nullable Sdk sdk) {
+    return sdk != null && isIdeaJdkType(sdk.getSdkType());
+  }
+
+  public static boolean isIdeaJdkType(SdkTypeId type) {
+    // return IdeaJdk.getInstance().equals(type);
+
+    // gross hack: SdkType.findInstance uses Class object equality, and for IJwB
+    // there are currently two copies of devkit classes...
+    return type.getName().equals("IDEA JDK");
+  }
+
+  @Nullable
+  public static String getBuildNumber(Sdk sdk) {
+    return IdeaJdk.getBuildNumber(sdk.getHomePath());
+  }
+
+  public static void copyIDEALicense(final String sandboxHome) {
+    IdeaLicenseHelper.copyIDEALicense(sandboxHome);
+  }
+
+  /**
+   * @throws RuntimeException if input Sdk is not an IdeaJdk
+   */
+  public static String getSandboxHome(Sdk sdk) {
+    if (!isIdeaJdk(sdk))
+      throw new RuntimeException("Invalid SDK type: " + sdk.getSdkType());
+    return ((Sandbox) sdk.getSdkAdditionalData()).getSandboxHome();
+  }
+
+  @Nullable
+  public static String getPlatformPrefix(String buildNumber) {
+    return IntelliJPlatformProduct.fromBuildNumber(buildNumber).getPlatformPrefix();
+  }
+
+}
diff --git a/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/IntellijWithPluginClasspathHelper.java b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/IntellijWithPluginClasspathHelper.java
new file mode 100644
index 0000000..7d88630
--- /dev/null
+++ b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/run/IntellijWithPluginClasspathHelper.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin.run;
+
+import com.google.common.collect.Lists;
+import com.intellij.execution.configurations.JavaParameters;
+import com.intellij.execution.configurations.ParametersList;
+import com.intellij.openapi.projectRoots.JavaSdkType;
+import com.intellij.openapi.projectRoots.Sdk;
+import com.intellij.openapi.util.SystemInfo;
+import com.intellij.util.PathsList;
+import org.jetbrains.annotations.NonNls;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Boilerplate for running an IJ application with an additional plugin,
+ * copied from org.jetbrains.idea.devkit.run.PluginRunConfiguration
+ */
+public class IntellijWithPluginClasspathHelper {
+
+  private static final List<String> IJ_LIBRARIES = Lists.newArrayList(
+    "log4j.jar",
+    "trove4j.jar",
+    "openapi.jar",
+    "util.jar",
+    "extensions.jar",
+    "bootstrap.jar",
+    "idea.jar",
+    "idea_rt.jar"
+  );
+
+  private static void addIntellijLibraries(JavaParameters params,
+                                           Sdk ideaJdk) {
+    @NonNls String libPath = ideaJdk.getHomePath() + File.separator + "lib";
+    PathsList list = params.getClassPath();
+    for (String lib : IJ_LIBRARIES) {
+      list.addFirst(libPath + File.separator + lib);
+    }
+    list.addFirst(((JavaSdkType) ideaJdk.getSdkType()).getToolsPath(ideaJdk));
+  }
+
+  public static void addRequiredVmParams(JavaParameters params,
+                                         Sdk ideaJdk) {
+    String canonicalSandbox = IdeaJdkHelper.getSandboxHome(ideaJdk);
+    ParametersList vm = params.getVMParametersList();
+
+    @NonNls String libPath = ideaJdk.getHomePath() + File.separator + "lib";
+    vm.add("-Xbootclasspath/a:" + libPath + File.separator + "boot.jar");
+
+    vm.defineProperty("idea.config.path", canonicalSandbox + File.separator + "config");
+    vm.defineProperty("idea.system.path", canonicalSandbox + File.separator + "system");
+    vm.defineProperty("idea.plugins.path", canonicalSandbox + File.separator + "plugins");
+    vm.defineProperty("idea.classpath.index.enabled", "false");
+
+    if (SystemInfo.isMac) {
+      vm.defineProperty("idea.smooth.progress", "false");
+      vm.defineProperty("apple.laf.useScreenMenuBar", "true");
+    }
+
+    if (SystemInfo.isXWindow) {
+      if (!vm.hasProperty("sun.awt.disablegrab")) {
+        vm.defineProperty("sun.awt.disablegrab", "true"); // See http://devnet.jetbrains.net/docs/DOC-1142
+      }
+    }
+
+    params.setWorkingDirectory(ideaJdk.getHomePath() + File.separator + "bin" + File.separator);
+    params.setJdk(ideaJdk);
+
+    addIntellijLibraries(params, ideaJdk);
+
+    params.setMainClass("com.intellij.idea.Main");
+  }
+}
diff --git a/blaze-plugin-dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
new file mode 100644
index 0000000..bf3d14b
--- /dev/null
+++ b/blaze-plugin-dev/src/com/google/idea/blaze/plugin/sync/IntellijPluginSyncPlugin.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin.sync;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.java.sync.JavaLanguageLevelHelper;
+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.roots.LanguageLevelProjectExtension;
+import com.intellij.pom.java.LanguageLevel;
+import com.intellij.util.ui.UIUtil;
+
+import javax.annotation.Nullable;
+import java.util.Set;
+
+/**
+ * Development environment support for intellij plugin projects.
+ * Prevents the project SDK being reset during sync
+ */
+public class IntellijPluginSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  @Nullable
+  @Override
+  public WorkspaceType getDefaultWorkspaceType() {
+    return WorkspaceType.INTELLIJ_PLUGIN;
+  }
+
+  @Nullable
+  @Override
+  public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.INTELLIJ_PLUGIN) {
+      return StdModuleTypes.JAVA;
+    }
+    return null;
+  }
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.INTELLIJ_PLUGIN) {
+      return ImmutableSet.of(LanguageClass.JAVA);
+    }
+    return ImmutableSet.of();
+  }
+
+  @Override
+  public void updateSdk(Project project,
+                        BlazeContext context,
+                        ProjectViewSet projectViewSet,
+                        BlazeProjectData blazeProjectData) {
+    if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.INTELLIJ_PLUGIN)) {
+      return;
+    }
+
+    LanguageLevel javaLanguageLevel = JavaLanguageLevelHelper
+      .getJavaLanguageLevel(projectViewSet, blazeProjectData, LanguageLevel.JDK_1_7);
+
+    // Leave the SDK, but set the language level
+    UIUtil.invokeAndWaitIfNeeded((Runnable)() -> ApplicationManager.getApplication().runWriteAction(() -> {
+      LanguageLevelProjectExtension ext = LanguageLevelProjectExtension.getInstance(project);
+      ext.setLanguageLevel(javaLanguageLevel);
+    }));
+  }
+}
diff --git a/blaze-plugin-dev/tests/integrationtests/com/google/idea/blaze/plugin/sync/PluginDevSyncTest.java b/blaze-plugin-dev/tests/integrationtests/com/google/idea/blaze/plugin/sync/PluginDevSyncTest.java
new file mode 100644
index 0000000..941947b
--- /dev/null
+++ b/blaze-plugin-dev/tests/integrationtests/com/google/idea/blaze/plugin/sync/PluginDevSyncTest.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.plugin.sync;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.RuleMapBuilder;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.sync.BlazeSyncIntegrationTestCase;
+import com.google.idea.blaze.base.sync.actions.IncrementalSyncProjectAction;
+import com.google.idea.blaze.base.sync.data.BlazeProjectDataManager;
+import com.google.idea.blaze.plugin.run.BlazeIntellijPluginConfiguration;
+import com.intellij.execution.RunManager;
+import com.intellij.execution.configurations.RunConfiguration;
+
+import java.util.List;
+
+import static com.google.common.truth.Truth.assertThat;
+
+/**
+ * Plugin-dev specific sync integration test.
+ */
+public class PluginDevSyncTest extends BlazeSyncIntegrationTestCase {
+  
+  public void ignore_testRunConfigurationCreatedDuringSync() throws Exception {
+    setProjectView(
+      "directories:",
+      "  java/com/google",
+      "targets:",
+      "  //java/com/google:lib",
+      "  //java/com/google:plugin",
+      "workspace_type: intellij_plugin"
+    );
+
+    createFile(
+      "java/com/google/ClassWithUniqueName1.java",
+      "package com.google;",
+      "public class ClassWithUniqueName1 {}"
+    );
+
+    createFile(
+      "java/com/google/ClassWithUniqueName2.java",
+      "package com.google;",
+      "public class ClassWithUniqueName2 {}"
+    );
+
+    ImmutableMap<Label, RuleIdeInfo> ruleMap = RuleMapBuilder.builder()
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                 .setLabel("//java/com/google:lib")
+                 .setKind("java_library")
+                 .addSource(sourceRoot("java/com/google/ClassWithUniqueName1.java"))
+                 .addSource(sourceRoot("java/com/google/ClassWithUniqueName2.java")))
+      .addRule(RuleIdeInfo.builder()
+                 .setBuildFile(sourceRoot("java/com/google/BUILD"))
+                 .setLabel("//java/com/google:plugin")
+                 .setKind("java_import")
+                 .addTag("intellij-plugin")
+      )
+      .build();
+
+    setRuleMap(ruleMap);
+
+    runBlazeSync(IncrementalSyncProjectAction.manualSyncParams);
+
+    assertNoErrors();
+
+    BlazeProjectData blazeProjectData = BlazeProjectDataManager.getInstance(getProject()).getBlazeProjectData();
+    assertThat(blazeProjectData).isNotNull();
+    assertThat(blazeProjectData.ruleMap).isEqualTo(ruleMap);
+    assertThat(blazeProjectData.workspaceLanguageSettings.getWorkspaceType())
+      .isEqualTo(WorkspaceType.INTELLIJ_PLUGIN);
+
+    List<RunConfiguration> runConfigs = RunManager.getInstance(getProject()).getAllConfigurationsList();
+    assertThat(runConfigs).hasSize(1);
+    assertThat(runConfigs.get(0)).isInstanceOf(BlazeIntellijPluginConfiguration.class);
+  }
+
+}
diff --git a/build_defs/BUILD b/build_defs/BUILD
new file mode 100644
index 0000000..47e9af1
--- /dev/null
+++ b/build_defs/BUILD
@@ -0,0 +1 @@
+package(default_visibility=["//visibility:public"])
diff --git a/build_defs/build_defs.bzl b/build_defs/build_defs.bzl
new file mode 100644
index 0000000..ada1bb5
--- /dev/null
+++ b/build_defs/build_defs.bzl
@@ -0,0 +1,65 @@
+# Copyright 2016 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+""" Description: Custom build macros for plugin.xml handling """
+#
+
+load("//build_defs/shared:build_defs.bzl",
+     "merged_plugin_xml_impl",
+     "stamped_plugin_xml_impl",
+     "intellij_plugin_impl")
+
+def merged_plugin_xml(name, srcs):
+    """Merges N plugin.xml files together
+    """
+    merged_plugin_xml_impl(
+        name = name,
+        srcs = srcs,
+        merge_tool = "//build_defs/shared:merge_xml",
+    )
+
+def stamped_plugin_xml(name, plugin_xml,
+                       stamp_since_build=False,
+                       stamp_until_build=False,
+                       version_file=None,
+                       changelog_file=None,
+                       include_product_code_in_stamp=False):
+    """Stamps a plugin xml file with the IJ build number.
+      stamp_since_build -- Add build number to idea-version since-build.
+      stamp_until_build -- Add build number to idea-version until-build.
+      version_file -- A file with the version number to be included.
+      changelog_file -- A file with changelog to be included.
+      include_product_code_in_stamp -- Whether the product code (eg. "IC")
+        is included in since-build and until-build.
+    """
+    stamped_plugin_xml_impl(
+        name = name,
+        build_txt = "//intellij-platform-sdk:build_number",
+        stamp_tool = "//build_defs/shared:stamp_plugin_xml",
+        plugin_xml = plugin_xml,
+        stamp_since_build = stamp_since_build,
+        version_file = version_file,
+        changelog_file = changelog_file,
+        include_product_code_in_stamp = include_product_code_in_stamp
+    )
+
+def intellij_plugin(name, plugin_xml, deps):
+    """ Creates an intellij plugin from the given deps and plugin.xml """
+    intellij_plugin_impl(
+        name = name,
+        plugin_xml = plugin_xml,
+        zip_tool = "//tools/zip",
+        deps = deps,
+    )
+
diff --git a/build_defs/shared/BUILD b/build_defs/shared/BUILD
new file mode 100644
index 0000000..60a6eaf
--- /dev/null
+++ b/build_defs/shared/BUILD
@@ -0,0 +1,15 @@
+# Description:
+#
+# Scripts for building IntelliJ plugins
+
+package(default_visibility = ["//visibility:public"])
+
+py_binary(
+    name = "merge_xml",
+    srcs = ["merge_xml.py"],
+)
+
+py_binary(
+    name = "stamp_plugin_xml",
+    srcs = ["stamp_plugin_xml.py"],
+)
diff --git a/build_defs/shared/build_defs.bzl b/build_defs/shared/build_defs.bzl
new file mode 100644
index 0000000..4d5712b
--- /dev/null
+++ b/build_defs/shared/build_defs.bzl
@@ -0,0 +1,131 @@
+# 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.
+
+"""Custom build macros for IntelliJ plugin handling.
+"""
+
+def merged_plugin_xml_impl(name, srcs, merge_tool):
+  """Merges N plugin.xml files together."""
+  native.genrule(
+      name = name,
+      srcs = srcs,
+      outs = [name + ".xml"],
+      cmd = "./$(location {merge_tool}) $(SRCS) > $@".format(
+          merge_tool=merge_tool,
+      ),
+      tools = [merge_tool],
+  )
+
+def _optstr(name, value):
+  return ("--" + name) if value else ""
+
+def stamped_plugin_xml_impl(name,
+                            plugin_xml,
+                            build_txt,
+                            stamp_tool,
+                            stamp_since_build=False,
+                            stamp_until_build=False,
+                            version_file=None,
+                            changelog_file=None,
+                            include_product_code_in_stamp=False):
+  """Stamps a plugin xml file with the IJ build number.
+
+  Args:
+    name: name of this rule
+    plugin_xml: target plugin_xml to stamp
+    build_txt: the file containing the build number
+    stamp_tool: the tool to use to stamp the version
+    stamp_since_build: Add build number to idea-version since-build.
+    stamp_until_build: Add build number to idea-version until-build.
+    version_file: A file with the version number to be included.
+    changelog_file: A file with the changelog to be included.
+    include_product_code_in_stamp: Whether the product code (eg. "IC")
+        is included in since-build and until-build.
+  """
+  args = [
+      "./$(location {stamp_tool})",
+      "--plugin_xml=$(location {plugin_xml})",
+      "--build_txt=$(location {build_txt})",
+      "{stamp_since_build}",
+      "{stamp_until_build}",
+      "{include_product_code_in_stamp}",
+  ]
+  srcs = [plugin_xml, build_txt]
+
+  if version_file:
+    args.append("--version_file=$(location {version_file})")
+    srcs.append(version_file)
+
+  if changelog_file:
+    args.append("--changelog_file=$(location {changelog_file})")
+    srcs.append(changelog_file)
+
+  cmd = " ".join(args).format(
+          plugin_xml=plugin_xml,
+          build_txt=build_txt,
+          stamp_tool=stamp_tool,
+          stamp_since_build=_optstr("stamp_since_build",
+                                    stamp_since_build),
+          stamp_until_build=_optstr("stamp_until_build",
+                                    stamp_until_build),
+          version_file=version_file,
+          changelog_file=changelog_file,
+          include_product_code_in_stamp=_optstr(
+              "include_product_code_in_stamp",
+              include_product_code_in_stamp)
+      ) + "> $@"
+
+  native.genrule(
+      name = name,
+      srcs = srcs,
+      outs = [name + ".xml"],
+      cmd = cmd,
+      tools = [stamp_tool],
+  )
+
+def intellij_plugin_impl(name, plugin_xml, zip_tool, deps):
+  """Creates an intellij plugin from the given deps and plugin.xml."""
+  binary_name = name + "_binary"
+  deploy_jar = binary_name + "_deploy.jar"
+  native.java_binary(
+      name = binary_name,
+      runtime_deps = deps,
+      create_executable = 0,
+  )
+  native.genrule(
+      name = name + "_genrule",
+      srcs = [plugin_xml, deploy_jar],
+      tools = [zip_tool],
+      outs = [name + ".jar"],
+      cmd = " ; ".join([
+          "cp $(location {deploy_jar}) $@",
+          "chmod +w $@",
+          "mkdir -p META-INF",
+          "cp $(location {plugin_xml}) META-INF/plugin.xml",
+          "$(location {zip_tool}) -u $@ META-INF/plugin.xml >/dev/null",
+      ]).format(
+          deploy_jar=deploy_jar,
+          plugin_xml=plugin_xml,
+          zip_tool=zip_tool,
+      ),
+  )
+
+  # included (with tag) as a hack so that IJwB can recognize this is an intellij plugin
+  native.java_import(
+      name = name,
+      jars = [name + ".jar"],
+      tags = ["intellij-plugin"],
+      visibility = ["//visibility:public"],
+  )
+
diff --git a/build_defs/shared/merge_xml.py b/build_defs/shared/merge_xml.py
new file mode 100755
index 0000000..d7332f6
--- /dev/null
+++ b/build_defs/shared/merge_xml.py
@@ -0,0 +1,55 @@
+# 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.
+
+"""Merges multiple xml files with the same top element tags into a single file.
+"""
+
+import sys
+from xml.dom.minidom import parse
+
+
+def AppendFileToTree(filepath, tree):
+  """Reads XML from a file and appends XML content to the tree.
+
+  Root elements for both trees must have the same tag.
+
+  Args:
+    filepath: Path to the file containing XML specification.
+    tree: Tree to add content to.
+
+  Raises:
+    RuntimeError: The top-level XML tags are incompatible
+  """
+
+  file_dom = parse(filepath)
+
+  if file_dom.documentElement.tagName != tree.documentElement.tagName:
+    raise RuntimeError("Incompatible top-level tags: '%s' vs. '%s'"
+                       % (file_dom.documentElement.tagName,
+                          tree.documentElement.tagName))
+
+  for node in file_dom.documentElement.childNodes:
+    tree.documentElement.appendChild(tree.importNode(node, True))
+
+
+if __name__ == "__main__":
+  if len(sys.argv) < 2:
+    print "Need xml filename(s) to be checked as parameter"
+    sys.exit(2)
+
+  dom = parse(sys.argv[1])
+  for filename in sys.argv[2:]:
+    AppendFileToTree(filename, dom)
+
+  print dom.toxml()
diff --git a/build_defs/shared/stamp_plugin_xml.py b/build_defs/shared/stamp_plugin_xml.py
new file mode 100755
index 0000000..96c384a
--- /dev/null
+++ b/build_defs/shared/stamp_plugin_xml.py
@@ -0,0 +1,135 @@
+# 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.
+
+"""Stamps a plugin xml with build information."""
+
+import argparse
+import re
+from xml.dom.minidom import parse
+
+parser = argparse.ArgumentParser()
+
+parser.add_argument(
+    "--plugin_xml",
+    help="The plugin xml file",
+    required=True,
+)
+parser.add_argument(
+    "--build_txt",
+    help="The build.txt file containing the build number, e.g. IC-144.1818",
+    required=True,
+)
+parser.add_argument(
+    "--stamp_since_build",
+    action="store_true",
+    help="Stamp since-build with the build number",
+)
+parser.add_argument(
+    "--stamp_until_build",
+    action="store_true",
+    help="Stamp until-build with the build number",
+)
+parser.add_argument(
+    "--version_file",
+    help="Version file to stamp into the plugin.xml",
+)
+parser.add_argument(
+    "--changelog_file",
+    help="Changelog file to add to plugin.xml",
+)
+parser.add_argument(
+    "--include_product_code_in_stamp",
+    action="store_true",
+    help="Include the product code in the stamp",
+)
+
+def _read_changelog(changelog_file):
+  """Reads the changelog and transforms it into trivial HTML"""
+  with open(changelog_file) as f:
+    lines = ["<p>" + line + "</p>" for line in f.readlines()]
+    return "\n".join(lines)
+
+
+def main():
+
+  args = parser.parse_args()
+
+  dom = parse(args.plugin_xml)
+
+  with open(args.build_txt) as f:
+    build_number = f.read()
+
+  new_elements = []
+
+  idea_plugin = dom.documentElement
+
+  match = re.match(r"^([A-Z]+-)?([0-9]+)(\.[0-9]+)?", build_number)
+  if match is None:
+    raise ValueError("Invalid build number: " + build_number)
+
+  build_number = match.group(1) + match.group(2) + match.group(3)
+  build_number_without_product_code = match.group(2) + match.group(3)
+
+  version_element = None
+  version_elements = idea_plugin.getElementsByTagName("version")
+  if len(version_elements) > 1:
+    raise ValueError("Ambigious version element")
+
+  if len(version_elements) == 1:
+    version_element = version_elements[0].firstChild
+
+  if args.version_file:
+    if version_element:
+      raise ValueError("version element already in plugin.xml")
+    version_element = dom.createElement("version")
+    new_elements.append(version_element)
+    with open(args.version_file) as f:
+      value = f.read().strip()
+      version_text = dom.createTextNode(value)
+      version_element.appendChild(version_text)
+
+  if args.stamp_since_build or args.stamp_until_build:
+    if idea_plugin.getElementsByTagName("idea-version"):
+      raise ValueError("idea-version element already present")
+
+    idea_version_build_element = (build_number
+                                  if args.include_product_code_in_stamp else
+                                  build_number_without_product_code)
+
+    idea_version_element = dom.createElement("idea-version")
+    new_elements.append(idea_version_element)
+
+    if args.stamp_since_build:
+      idea_version_element.setAttribute("since-build",
+                                        idea_version_build_element)
+    if args.stamp_until_build:
+      idea_version_element.setAttribute("until-build",
+                                        idea_version_build_element)
+
+  if args.changelog_file:
+    if idea_plugin.getElementsByTagName("change-notes"):
+      raise ValueError("change-notes element already in plugin.xml")
+    changelog_element = dom.createElement("change-notes")
+    changelog_text = dom.createCDATASection(_read_changelog(args.changelog_file))
+    changelog_element.appendChild(changelog_text)
+    new_elements.append(changelog_element)
+
+  for new_element in new_elements:
+    idea_plugin.appendChild(new_element)
+
+  print dom.toxml()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/ijwb/.bazelproject b/ijwb/.bazelproject
new file mode 100644
index 0000000..3dc3303
--- /dev/null
+++ b/ijwb/.bazelproject
@@ -0,0 +1,16 @@
+directories:
+  .
+  -aswb
+  -clwb
+  -blaze-cpp
+
+targets:
+  //ijwb:ijwb_bazel
+  //ijwb:ijwb_lib
+  //:ijwb_tests
+
+workspace_type: intellij_plugin
+
+test_sources:
+  */tests/unittests*
+  */tests/integrationtests*
diff --git a/ijwb/.gitignore b/ijwb/.gitignore
new file mode 100644
index 0000000..bec2fed
--- /dev/null
+++ b/ijwb/.gitignore
@@ -0,0 +1,8 @@
+.idea/workspace.xml
+.idea/tasks.xml
+out
+bazel-bin
+bazel-out
+bazel-genfiles
+bazel-testlogs
+bazel-blaze
diff --git a/ijwb/BUILD b/ijwb/BUILD
new file mode 100644
index 0000000..c681b39
--- /dev/null
+++ b/ijwb/BUILD
@@ -0,0 +1,58 @@
+#
+# Description: Builds ijwb
+#
+
+load(
+    "//build_defs:build_defs.bzl",
+    "merged_plugin_xml",
+    "stamped_plugin_xml",
+    "intellij_plugin",
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml_common",
+    srcs = [
+        "src/META-INF/ijwb.xml",
+        "//blaze-base:plugin_xml",
+        "//blaze-java:plugin_xml",
+        "//blaze-plugin-dev:plugin_xml",
+    ],
+)
+
+merged_plugin_xml(
+    name = "merged_plugin_xml_bazel",
+    srcs = [
+        "src/META-INF/ijwb_bazel.xml",
+        ":merged_plugin_xml_common",
+    ],
+)
+
+stamped_plugin_xml(
+    name = "stamped_plugin_xml_bazel",
+    include_product_code_in_stamp = True,
+    plugin_xml = ":merged_plugin_xml_bazel",
+    stamp_since_build = True,
+    version_file = "//:version",
+)
+
+java_library(
+    name = "ijwb_lib",
+    srcs = glob(["src/**/*.java"]),
+    exports = [
+        "//blaze-plugin-dev",
+    ],
+    deps = [
+        "//blaze-base",
+        "//blaze-java",
+        "//intellij-platform-sdk:plugin_api",
+        "//third_party:jsr305",
+    ],
+)
+
+intellij_plugin(
+    name = "ijwb_bazel",
+    plugin_xml = ":stamped_plugin_xml_bazel",
+    deps = [
+        ":ijwb_lib",
+    ],
+)
diff --git a/ijwb/src/META-INF/ijwb.xml b/ijwb/src/META-INF/ijwb.xml
new file mode 100644
index 0000000..988e14e
--- /dev/null
+++ b/ijwb/src/META-INF/ijwb.xml
@@ -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.
+  -->
+<idea-plugin>
+  <id>com.google.idea.blaze.ijwb</id>
+  <vendor>Google</vendor>
+
+  <extensions defaultExtensionNs="com.intellij">
+    <applicationService serviceInterface="com.google.idea.blaze.base.plugin.BlazePluginId"
+                        serviceImplementation="com.google.idea.blaze.ijwb.plugin.IjwbPluginId"/>
+  </extensions>
+
+  <extensions defaultExtensionNs="com.google.idea.blaze">
+    <SyncPlugin implementation="com.google.idea.blaze.ijwb.android.BlazeAndroidLiteSyncPlugin"/>
+    <SyncPlugin implementation="com.google.idea.blaze.ijwb.javascript.BlazeJavascriptSyncPlugin"/>
+    <SyncPlugin implementation="com.google.idea.blaze.ijwb.typescript.BlazeTypescriptSyncPlugin"/>
+    <SyncPlugin implementation="com.google.idea.blaze.ijwb.dart.BlazeDartSyncPlugin"/>
+    <java.JavaSyncAugmenter implementation="com.google.idea.blaze.ijwb.android.BlazeAndroidLiteJavaSyncAugmenter"/>
+    <java.JavaSyncAugmenter implementation="com.google.idea.blaze.ijwb.typescript.BlazeTypescriptJavaSyncAugmenter"/>
+    <java.JavaSyncAugmenter implementation="com.google.idea.blaze.ijwb.dart.BlazeDartJavaSyncAugmenter"/>
+  </extensions>
+
+</idea-plugin>
diff --git a/ijwb/src/META-INF/ijwb_bazel.xml b/ijwb/src/META-INF/ijwb_bazel.xml
new file mode 100644
index 0000000..a258cab
--- /dev/null
+++ b/ijwb/src/META-INF/ijwb_bazel.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+
+  <name>IntelliJ with Bazel</name>
+
+  <description>
+    Provides the ability to import Bazel Java projects in IntelliJ.
+  </description>
+
+</idea-plugin>
\ No newline at end of file
diff --git a/ijwb/src/META-INF/ijwb_blaze.xml b/ijwb/src/META-INF/ijwb_blaze.xml
new file mode 100644
index 0000000..eaef611
--- /dev/null
+++ b/ijwb/src/META-INF/ijwb_blaze.xml
@@ -0,0 +1,24 @@
+<!--
+  ~ Copyright 2016 The Bazel Authors. All rights reserved.
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~    http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<idea-plugin>
+
+  <name>IntelliJ with Blaze</name>
+
+  <description>
+    Provides the ability to import Blaze Java projects in IntelliJ.
+  </description>
+
+</idea-plugin>
\ No newline at end of file
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteImportResult.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteImportResult.java
new file mode 100644
index 0000000..05c2b10
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteImportResult.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.ijwb.android;
+
+import com.google.common.collect.ImmutableCollection;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.Serializable;
+
+/**
+ * The result of a blaze import operation.
+ */
+@Immutable
+public class BlazeAndroidLiteImportResult implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final ImmutableCollection<BlazeLibrary> libraries;
+
+  public BlazeAndroidLiteImportResult(
+    ImmutableCollection<BlazeLibrary> libraries) {
+    this.libraries = libraries;
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
new file mode 100644
index 0000000..fabfb7d
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteJavaSyncAugmenter.java
@@ -0,0 +1,48 @@
+/*
+ * 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.android;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.projectview.section.Glob;
+import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+
+import java.util.Collection;
+
+/**
+ * Augments the java sync process with Android lite support.
+ */
+public class BlazeAndroidLiteJavaSyncAugmenter implements BlazeJavaSyncAugmenter {
+
+  @Override
+  public void addLibraryFilter(Glob.GlobSet excludedLibraries) {
+  }
+
+  @Override
+  public Collection<BlazeLibrary> getAdditionalLibraries(BlazeProjectData blazeProjectData) {
+    BlazeAndroidLiteSyncData syncData = blazeProjectData.syncState.get(BlazeAndroidLiteSyncData.class);
+    if (syncData == null) {
+      return ImmutableList.of();
+    }
+    return syncData.importResult.libraries;
+  }
+
+  @Override
+  public Collection<String> getExternallyAddedLibraries(BlazeProjectData blazeProjectData) {
+    return ImmutableList.of();
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncData.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncData.java
new file mode 100644
index 0000000..b510f05
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncData.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.ijwb.android;
+
+import javax.annotation.concurrent.Immutable;
+import java.io.Serializable;
+
+/**
+ * Sync data for the Android lite plugin.
+ */
+@Immutable
+public class BlazeAndroidLiteSyncData implements Serializable {
+  private static final long serialVersionUID = 1L;
+
+  public final BlazeAndroidLiteImportResult importResult;
+
+  public BlazeAndroidLiteSyncData(BlazeAndroidLiteImportResult importResult) {
+    this.importResult = importResult;
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java
new file mode 100644
index 0000000..a799d7e
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteSyncPlugin.java
@@ -0,0 +1,88 @@
+/*
+ * 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.android;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.model.primitives.WorkspaceType;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.scopes.TimingScope;
+import com.google.idea.blaze.base.sync.BlazeSyncPlugin;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+import com.intellij.openapi.project.Project;
+
+import javax.annotation.Nullable;
+import java.io.File;
+import java.util.Set;
+
+/**
+ * Rudimentary support for android in IntelliJ.
+ */
+public class BlazeAndroidLiteSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    switch (workspaceType) {
+      case ANDROID:
+      case JAVA:
+        return ImmutableSet.of(LanguageClass.ANDROID);
+      default:
+        return ImmutableSet.of();
+    }
+  }
+
+  @Override
+  public void updateSyncState(Project project,
+                              BlazeContext context,
+                              WorkspaceRoot workspaceRoot,
+                              ProjectViewSet projectViewSet,
+                              WorkspaceLanguageSettings workspaceLanguageSettings,
+                              BlazeRoots blazeRoots,
+                              @Nullable WorkingSet workingSet,
+                              WorkspacePathResolver workspacePathResolver,
+                              ImmutableMap<Label, RuleIdeInfo> ruleMap,
+                              @Deprecated @Nullable File androidPlatformDirectory,
+                              SyncState.Builder syncStateBuilder,
+                              @Nullable SyncState previousSyncState) {
+    if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.ANDROID)) {
+      return;
+    }
+
+    BlazeAndroidLiteWorkspaceImporter workspaceImporter = new BlazeAndroidLiteWorkspaceImporter(
+      project,
+      workspaceRoot,
+      context,
+      projectViewSet,
+      ruleMap
+    );
+    BlazeAndroidLiteImportResult importResult = Scope.push(context, childContext -> {
+      childContext.push(new TimingScope("AndroidLiteWorkspaceImporter"));
+      return workspaceImporter.importWorkspace();
+    });
+    BlazeAndroidLiteSyncData syncData = new BlazeAndroidLiteSyncData(importResult);
+    syncStateBuilder.put(BlazeAndroidLiteSyncData.class, syncData);
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteWorkspaceImporter.java b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteWorkspaceImporter.java
new file mode 100644
index 0000000..c5fd4eb
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/android/BlazeAndroidLiteWorkspaceImporter.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.ijwb.android;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.idea.blaze.base.ideinfo.AndroidRuleIdeInfo;
+import com.google.idea.blaze.base.ideinfo.LibraryArtifact;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.model.primitives.WorkspaceRoot;
+import com.google.idea.blaze.base.projectview.ProjectViewSet;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.sync.projectview.ProjectViewRuleImportFilter;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+import com.google.idea.blaze.java.sync.model.LibraryKey;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.Project;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * Builds a BlazeWorkspace.
+ */
+public final class BlazeAndroidLiteWorkspaceImporter {
+  private static final Logger LOG = Logger.getInstance(BlazeAndroidLiteWorkspaceImporter.class);
+
+  private final Project project;
+  private final BlazeContext context;
+  private final ImmutableMap<Label, RuleIdeInfo> ruleMap;
+  private final ProjectViewRuleImportFilter importFilter;
+
+  public BlazeAndroidLiteWorkspaceImporter(
+    Project project,
+    WorkspaceRoot workspaceRoot,
+    BlazeContext context,
+    ProjectViewSet projectViewSet,
+    ImmutableMap<Label, RuleIdeInfo> ruleMap) {
+    this.project = project;
+    this.context = context;
+    this.ruleMap = ruleMap;
+    this.importFilter = new ProjectViewRuleImportFilter(project, workspaceRoot, projectViewSet);
+  }
+
+  public BlazeAndroidLiteImportResult importWorkspace() {
+    List<RuleIdeInfo> rules = ruleMap.values()
+      .stream()
+      .filter(importFilter::isSourceRule)
+      .filter(rule -> rule.kind.getLanguageClass() == LanguageClass.ANDROID)
+      .filter(rule -> !importFilter.excludeTarget(rule))
+      .collect(Collectors.toList());
+
+    WorkspaceBuilder workspaceBuilder = new WorkspaceBuilder();
+
+    for (RuleIdeInfo rule : rules) {
+      addRuleAsSource(
+        workspaceBuilder,
+        rule
+      );
+    }
+
+    return new BlazeAndroidLiteImportResult(
+      workspaceBuilder.libraries.build()
+    );
+  }
+
+  private void addRuleAsSource(
+    WorkspaceBuilder workspaceBuilder,
+    RuleIdeInfo rule) {
+
+    AndroidRuleIdeInfo androidRuleIdeInfo = rule.androidRuleIdeInfo;
+    if (androidRuleIdeInfo != null) {
+      // Add R.java jars
+      LibraryArtifact resourceJar = androidRuleIdeInfo.resourceJar;
+      if (resourceJar != null) {
+        BlazeLibrary library1 = new BlazeLibrary(LibraryKey.fromJarFile(resourceJar.jar.getFile()), resourceJar);
+        workspaceBuilder.libraries.add(library1);
+      }
+
+      LibraryArtifact idlJar = androidRuleIdeInfo.idlJar;
+      if (idlJar != null) {
+        BlazeLibrary library = new BlazeLibrary(LibraryKey.fromJarFile(idlJar.jar.getFile()), idlJar);
+        workspaceBuilder.libraries.add(library);
+      }
+    }
+  }
+
+  static class WorkspaceBuilder {
+    ImmutableList.Builder<BlazeLibrary> libraries = ImmutableList.builder();
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartJavaSyncAugmenter.java b/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartJavaSyncAugmenter.java
new file mode 100644
index 0000000..0de1f96
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartJavaSyncAugmenter.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.ijwb.dart;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.projectview.section.Glob;
+import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+
+import java.util.Collection;
+
+/**
+ * Prevents garbage collection of the Dart SDK library.
+ */
+public class BlazeDartJavaSyncAugmenter implements BlazeJavaSyncAugmenter {
+  @Override
+  public void addLibraryFilter(Glob.GlobSet excludedLibraries) {
+  }
+
+  @Override
+  public Collection<BlazeLibrary> getAdditionalLibraries(BlazeProjectData blazeProjectData) {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<String> getExternallyAddedLibraries(BlazeProjectData blazeProjectData) {
+    if (blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.DART)) {
+      return ImmutableList.of(BlazeDartSyncPlugin.DART_SDK_LIBRARY_NAME);
+    }
+    return ImmutableList.of();
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPlugin.java
new file mode 100644
index 0000000..81fbf2b
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/dart/BlazeDartSyncPlugin.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.ijwb.dart;
+
+import com.google.common.collect.ImmutableSet;
+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.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.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.ijwb.ide.IdeCheck;
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.impl.libraries.ApplicationLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+
+import javax.annotation.Nullable;
+import java.util.Set;
+
+/**
+ * Supports dart.
+ */
+public class BlazeDartSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  static final String DART_SDK_LIBRARY_NAME = "Dart SDK";
+  private static final String DART_PLUGIN_ID = "Dart";
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    return ImmutableSet.of(LanguageClass.DART);
+  }
+
+  @Override
+  public void updateProjectStructure(Project project,
+                                     BlazeContext context,
+                                     WorkspaceRoot workspaceRoot,
+                                     ProjectViewSet projectViewSet,
+                                     BlazeProjectData blazeProjectData,
+                                     @Nullable BlazeProjectData oldBlazeProjectData,
+                                     ModuleEditor moduleEditor,
+                                     Module workspaceModule,
+                                     ModifiableRootModel workspaceModifiableModel) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.DART)) {
+      return;
+    }
+
+    Library dartSdkLibrary = ApplicationLibraryTable.getApplicationTable().getLibraryByName(DART_SDK_LIBRARY_NAME);
+    if (dartSdkLibrary != null) {
+      if (workspaceModifiableModel.findLibraryOrderEntry(dartSdkLibrary) == null) {
+        workspaceModifiableModel.addLibraryEntry(dartSdkLibrary);
+      }
+    } else {
+      IssueOutput
+        .error("Dart language support is requested, but the Dart SDK was not found. "
+               + "You must manually enable Dart support from File > Settings > Languages & Frameworks > Dart.")
+        .submit(context);
+    }
+  }
+
+  @Override
+  public boolean validateProjectView(BlazeContext context,
+                                     ProjectViewSet projectViewSet,
+                                     WorkspaceLanguageSettings workspaceLanguageSettings) {
+    if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.DART)) {
+      return true;
+    }
+    if (!IdeCheck.isPluginEnabled(DART_PLUGIN_ID)) {
+      IssueOutput.error("Dart plugin needed for Dart language support.").submit(context);
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/ide/IdeCheck.java b/ijwb/src/com/google/idea/blaze/ijwb/ide/IdeCheck.java
new file mode 100644
index 0000000..33a94ec
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/ide/IdeCheck.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.ijwb.ide;
+
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.openapi.extensions.PluginId;
+
+/**
+ * IDE and plugin checks.
+ */
+public class IdeCheck {
+  public static boolean isPluginEnabled(String pluginIdString) {
+    PluginId pluginId = PluginId.getId(pluginIdString);
+    if (!PluginManager.isPluginInstalled(pluginId)) {
+      return false;
+    }
+    IdeaPluginDescriptor plugin = PluginManager.getPlugin(pluginId);
+    if (plugin == null) {
+      return false;
+    }
+    return plugin.isEnabled();
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java
new file mode 100644
index 0000000..b41596e
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/javascript/BlazeJavascriptSyncPlugin.java
@@ -0,0 +1,99 @@
+/*
+ * 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 com.google.common.collect.ImmutableSet;
+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.projectview.SourceTestConfig;
+import com.google.idea.blaze.base.sync.projectview.WorkspaceLanguageSettings;
+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.vfs.VirtualFile;
+import com.intellij.util.PlatformUtils;
+
+import javax.annotation.Nullable;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Allows people to use a javascript-only workspace.
+ */
+public class BlazeJavascriptSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  @Nullable
+  @Override
+  public ModuleType getWorkspaceModuleType(WorkspaceType workspaceType) {
+    if (workspaceType == WorkspaceType.JAVASCRIPT) {
+      return WebModuleType.getInstance();
+    }
+    return null;
+  }
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    return ImmutableSet.of(LanguageClass.JAVASCRIPT);
+  }
+
+  @Override
+  public void updateContentEntries(Project project,
+                                   BlazeContext context,
+                                   WorkspaceRoot workspaceRoot,
+                                   ProjectViewSet projectViewSet,
+                                   BlazeProjectData blazeProjectData,
+                                   Collection<ContentEntry> contentEntries) {
+    if (!blazeProjectData.workspaceLanguageSettings.isWorkspaceType(WorkspaceType.JAVASCRIPT)) {
+      return;
+    }
+
+    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);
+    }
+  }
+
+  @Override
+  public boolean validateProjectView(BlazeContext context,
+                                     ProjectViewSet projectViewSet,
+                                     WorkspaceLanguageSettings workspaceLanguageSettings) {
+    if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.JAVASCRIPT)) {
+      return true;
+    }
+    if (!PlatformUtils.isIdeaUltimate()) {
+      IssueOutput.error("IntelliJ Ultimate needed for Javascript support.").submit(context);
+      return false;
+    }
+    return true;
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/plugin/IjwbPluginId.java b/ijwb/src/com/google/idea/blaze/ijwb/plugin/IjwbPluginId.java
new file mode 100644
index 0000000..bfaba31
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/plugin/IjwbPluginId.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.ijwb.plugin;
+
+import com.google.idea.blaze.base.plugin.BlazePluginId;
+
+/**
+ * IJwB plugin configuration information.
+ */
+public class IjwbPluginId implements BlazePluginId {
+
+  private static final String PLUGIN_ID = "com.google.idea.blaze.ijwb";  // Please keep up-to-date with plugin.xml
+
+  @Override
+  public String getPluginId() {
+    return PLUGIN_ID;
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptJavaSyncAugmenter.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptJavaSyncAugmenter.java
new file mode 100644
index 0000000..251b04d
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptJavaSyncAugmenter.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.ijwb.typescript;
+
+import com.google.common.collect.ImmutableList;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+import com.google.idea.blaze.base.projectview.section.Glob;
+import com.google.idea.blaze.java.sync.BlazeJavaSyncAugmenter;
+import com.google.idea.blaze.java.sync.model.BlazeLibrary;
+
+import java.util.Collection;
+
+/**
+ * Prevents garbage collection of "tsconfig$roots"
+ */
+public class BlazeTypescriptJavaSyncAugmenter implements BlazeJavaSyncAugmenter {
+  @Override
+  public void addLibraryFilter(Glob.GlobSet excludedLibraries) {
+  }
+
+  @Override
+  public Collection<BlazeLibrary> getAdditionalLibraries(BlazeProjectData blazeProjectData) {
+    return ImmutableList.of();
+  }
+
+  @Override
+  public Collection<String> getExternallyAddedLibraries(BlazeProjectData blazeProjectData) {
+    if (blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.TYPESCRIPT)) {
+      return ImmutableList.of(BlazeTypescriptSyncPlugin.TSCONFIG_LIBRARY_NAME);
+    }
+    return ImmutableList.of();
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
new file mode 100644
index 0000000..7e5c3e7
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/BlazeTypescriptSyncPlugin.java
@@ -0,0 +1,172 @@
+/*
+ * 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.typescript;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.idea.blaze.base.async.process.ExternalTask;
+import com.google.idea.blaze.base.async.process.LineProcessingOutputStream;
+import com.google.idea.blaze.base.command.BlazeCommand;
+import com.google.idea.blaze.base.command.BlazeCommandName;
+import com.google.idea.blaze.base.command.BlazeFlags;
+import com.google.idea.blaze.base.ideinfo.RuleIdeInfo;
+import com.google.idea.blaze.base.issueparser.IssueOutputLineProcessor;
+import com.google.idea.blaze.base.model.BlazeProjectData;
+import com.google.idea.blaze.base.model.SyncState;
+import com.google.idea.blaze.base.model.primitives.Label;
+import com.google.idea.blaze.base.model.primitives.LanguageClass;
+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.projectview.section.SectionParser;
+import com.google.idea.blaze.base.scope.BlazeContext;
+import com.google.idea.blaze.base.scope.Scope;
+import com.google.idea.blaze.base.scope.output.IssueOutput;
+import com.google.idea.blaze.base.scope.output.PrintOutput;
+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.projectview.WorkspaceLanguageSettings;
+import com.google.idea.blaze.base.sync.workspace.BlazeRoots;
+import com.google.idea.blaze.base.sync.workspace.WorkingSet;
+import com.google.idea.blaze.base.sync.workspace.WorkspacePathResolver;
+
+import com.intellij.openapi.module.Module;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.roots.ModifiableRootModel;
+import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
+import com.intellij.openapi.roots.libraries.Library;
+import com.intellij.util.PlatformUtils;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/**
+ * Supports typescript.
+ */
+public class BlazeTypescriptSyncPlugin extends BlazeSyncPlugin.Adapter {
+
+  static final String TSCONFIG_LIBRARY_NAME = "tsconfig$roots";
+
+  @Override
+  public Set<LanguageClass> getSupportedLanguagesInWorkspace(WorkspaceType workspaceType) {
+    return ImmutableSet.of(LanguageClass.TYPESCRIPT);
+  }
+
+  @Override
+  public void updateSyncState(Project project,
+                              BlazeContext context,
+                              WorkspaceRoot workspaceRoot,
+                              ProjectViewSet projectViewSet,
+                              WorkspaceLanguageSettings workspaceLanguageSettings,
+                              BlazeRoots blazeRoots,
+                              @Nullable WorkingSet workingSet,
+                              WorkspacePathResolver workspacePathResolver,
+                              ImmutableMap<Label, RuleIdeInfo> ruleMap,
+                              @Deprecated @Nullable File androidPlatformDirectory,
+                              SyncState.Builder syncStateBuilder,
+                              @Nullable SyncState previousSyncState) {
+    if (!workspaceLanguageSettings.isLanguageActive(LanguageClass.TYPESCRIPT)) {
+      return;
+    }
+
+    Label tsConfig = projectViewSet.getSectionValue(TsConfigRuleSection.KEY);
+    if (tsConfig == null) {
+      invalidProjectViewError(context);
+      return;
+    }
+
+    Scope.push(context, (childContext) -> {
+      childContext.push(new TimingScope("TsConfig"));
+      childContext.output(PrintOutput.output("Updating tsconfig..."));
+
+      BlazeCommand command = BlazeCommand.builder(Blaze.getBuildSystem(project), BlazeCommandName.RUN)
+        .addTargets(tsConfig)
+        .addBlazeFlags(BlazeFlags.buildFlags(project, projectViewSet))
+        .build();
+
+      int retVal = ExternalTask.builder(workspaceRoot, command)
+        .context(childContext)
+        .stderr(LineProcessingOutputStream.of(
+          new IssueOutputLineProcessor(project, childContext, workspaceRoot)
+        ))
+        .build()
+        .run();
+
+      if (retVal != 0) {
+        childContext.setHasError();
+      }
+    });
+  }
+
+  @Override
+  public void updateProjectStructure(Project project,
+                                     BlazeContext context,
+                                     WorkspaceRoot workspaceRoot,
+                                     ProjectViewSet projectViewSet,
+                                     BlazeProjectData blazeProjectData,
+                                     @Nullable BlazeProjectData oldBlazeProjectData,
+                                     ModuleEditor moduleEditor,
+                                     Module workspaceModule,
+                                     ModifiableRootModel workspaceModifiableModel) {
+    if (!blazeProjectData.workspaceLanguageSettings.isLanguageActive(LanguageClass.TYPESCRIPT)) {
+      return;
+    }
+
+    Library tsConfigLibrary = ProjectLibraryTable.getInstance(project).getLibraryByName(TSCONFIG_LIBRARY_NAME);
+    if (tsConfigLibrary != null) {
+      if (workspaceModifiableModel.findLibraryOrderEntry(tsConfigLibrary) == null) {
+        workspaceModifiableModel.addLibraryEntry(tsConfigLibrary);
+      }
+    }
+  }
+
+  @Override
+  public boolean validateProjectView(BlazeContext context,
+                                     ProjectViewSet projectViewSet,
+                                     WorkspaceLanguageSettings workspaceLanguageSettings) {
+    boolean typescriptActive = workspaceLanguageSettings.isLanguageActive(LanguageClass.TYPESCRIPT);
+
+    if (typescriptActive && !PlatformUtils.isIdeaUltimate()) {
+      IssueOutput.error("IntelliJ Ultimate needed for Typescript support.").submit(context);
+      return false;
+    }
+
+    // Must have either both typescript and ts_config_rule or neither
+    Label tsConfig = projectViewSet.getSectionValue(TsConfigRuleSection.KEY);
+    if (typescriptActive ^ (tsConfig != null)) {
+      invalidProjectViewError(context);
+      return false;
+    }
+
+    return true;
+  }
+
+  private void invalidProjectViewError(BlazeContext context) {
+    IssueOutput
+      .error("For Typescript support you must add both additional_languages: typescript and the ts_config_rule attribute.")
+      .submit(context);
+  }
+
+  @Override
+  public Collection<SectionParser> getSections() {
+    return ImmutableList.of(TsConfigRuleSection.PARSER);
+  }
+}
diff --git a/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRuleSection.java b/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRuleSection.java
new file mode 100644
index 0000000..ea476fd
--- /dev/null
+++ b/ijwb/src/com/google/idea/blaze/ijwb/typescript/TsConfigRuleSection.java
@@ -0,0 +1,64 @@
+/*
+ * 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.typescript;
+
+import com.google.common.collect.Lists;
+import com.google.idea.blaze.base.model.primitives.Label;
+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.ui.BlazeValidationError;
+
+import javax.annotation.Nullable;
+import java.util.List;
+
+/**
+ * Points to the ts_config rule.
+ */
+public class TsConfigRuleSection {
+  public static final SectionKey<Label, ScalarSection<Label>> KEY = SectionKey.of("ts_config_rule");
+  public static final SectionParser PARSER = new TsConfigRuleSectionParser();
+
+  private static class TsConfigRuleSectionParser extends ScalarSectionParser<Label> {
+    public TsConfigRuleSectionParser() {
+      super(KEY, ':');
+    }
+
+    @Nullable
+    @Override
+    protected Label parseItem(ProjectViewParser parser, ParseContext parseContext, String rest) {
+      List<BlazeValidationError> errors = Lists.newArrayList();
+      if (!Label.validate(rest, errors)) {
+        parseContext.addErrors(errors);
+        return null;
+      }
+      return new Label(rest);
+    }
+
+    @Override
+    protected void printItem(StringBuilder sb, Label value) {
+      sb.append(value.toString());
+    }
+
+    @Override
+    public ItemType getItemType() {
+      return ItemType.Label;
+    }
+  }
+}
diff --git a/intellij-platform-sdk/BUILD b/intellij-platform-sdk/BUILD
new file mode 100644
index 0000000..617aa82
--- /dev/null
+++ b/intellij-platform-sdk/BUILD
@@ -0,0 +1,100 @@
+#
+# Description: IntelliJ plugin SDKs required to build the plugin jars.
+#
+
+config_setting(
+    name = "intellij-latest",
+    values = {
+        "define": "ij_product=intellij-latest",
+    },
+)
+
+config_setting(
+    name = "clion-latest",
+    values = {
+        "define": "ij_product=clion-latest",
+    },
+)
+
+config_setting(
+    name = "android-studio-latest",
+    values = {
+        "define": "ij_product=android-studio-latest",
+    },
+)
+
+java_library(
+    name = "plugin_api_internal",
+    exports = select({
+        ":intellij-latest": ["@intellij_latest//:plugin_api"],
+        ":clion-latest": ["@clion_latest//:plugin_api"],
+        ":android-studio-latest": ["//intellij-platform-sdk/AI-145.971.21:plugin_api"],
+        "//conditions:default": ["@intellij_latest//:plugin_api"],
+    }),
+)
+
+# The outward facing plugin api
+java_library(
+    name = "plugin_api",
+    neverlink = 1,
+    visibility = ["//visibility:public"],
+    exports = [":plugin_api_internal"],
+)
+
+# for tests, we need the IJ API at runtime,
+# so can't use the neverlink rule
+java_library(
+    name = "plugin_api_for_tests",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = [":plugin_api_internal"],
+)
+
+# The dev kit is only for IntelliJ since you only develop plugins in Java.
+java_library(
+    name = "devkit",
+    neverlink = 1,
+    visibility = ["//visibility:public"],
+    exports = select({
+        ":intellij-latest": ["@intellij_latest//:devkit"],
+        "//conditions:default": ["@intellij_latest//:devkit"],
+    }),
+)
+
+filegroup(
+    name = "build_number",
+    srcs = select({
+        ":intellij-latest": ["@intellij_latest//:build_number"],
+        ":clion-latest": ["@clion_latest//:build_number"],
+        ":android-studio-latest": ["//intellij-platform-sdk/AI-145.971.21:build_number"],
+        "//conditions:default": ["@intellij_latest//:build_number"],
+    }),
+    visibility = ["//visibility:public"],
+)
+
+# Plugins bundled with the SDK which are required for compilation and/or integration tests
+java_library(
+    name = "bundled_plugins_internal",
+    exports = select({
+        ":intellij-latest": ["@intellij_latest//:bundled_plugins"],
+        ":clion-latest": ["@clion_latest//:bundled_plugins"],
+        ":android-studio-latest": ["//intellij-platform-sdk/AI-145.971.21:bundled_plugins"],
+        "//conditions:default": ["@intellij_latest//:bundled_plugins"],
+    }),
+)
+
+java_library(
+    name = "bundled_plugins",
+    neverlink = 1,
+    visibility = ["//visibility:public"],
+    exports = [":bundled_plugins_internal"],
+)
+
+# for tests, we include the bundled plugins at runtime,
+# so can't use the neverlink rule
+java_library(
+    name = "bundled_plugins_for_tests",
+    testonly = 1,
+    visibility = ["//visibility:public"],
+    exports = [":bundled_plugins_internal"],
+)
diff --git a/intellij_test/BUILD b/intellij_test/BUILD
new file mode 100644
index 0000000..cfd25b4
--- /dev/null
+++ b/intellij_test/BUILD
@@ -0,0 +1,15 @@
+# Description:
+#
+# Common test utilities for IntelliJ plugins.
+
+package(default_visibility = ["//visibility:public"])
+
+java_library(
+    name = "lib",
+    srcs = glob(["src/**/*.java"]),
+    deps = [
+        "//intellij-platform-sdk:plugin_api_for_tests",
+        "//third_party:jsr305",
+        "//third_party:test_lib",
+    ],
+)
diff --git a/intellij_test/src/com/google/idea/blaze/base/BlazeTestSystemProperties.java b/intellij_test/src/com/google/idea/blaze/base/BlazeTestSystemProperties.java
new file mode 100644
index 0000000..2a19f58
--- /dev/null
+++ b/intellij_test/src/com/google/idea/blaze/base/BlazeTestSystemProperties.java
@@ -0,0 +1,152 @@
+/*
+ * 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;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.Lists;
+
+import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess;
+import com.intellij.util.PlatformUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * Test utilities specific to running in a blaze/bazel environment.
+ */
+public class BlazeTestSystemProperties {
+
+  /**
+   * The absolute path to the runfiles directory.
+   */
+  private static final String RUNFILES_PATH = getUserValue("TEST_SRCDIR");
+
+  public static boolean isRunThroughBlaze() {
+    return System.getenv("JAVA_RUNFILES") != null;
+  }
+
+  /**
+   * Sets up the necessary system properties for running IntelliJ tests via blaze/bazel.
+   */
+  public static void configureSystemProperties() throws IOException {
+    if (!isRunThroughBlaze()) {
+      return;
+    }
+    File sandbox = new File(getTmpDirFile(), "_intellij_test_sandbox");
+
+    setSandboxPath("idea.home.path", new File(sandbox, "home"));
+    setSandboxPath("idea.config.path", new File(sandbox, "config"));
+    setSandboxPath("idea.system.path", new File(sandbox, "system"));
+    setIfEmpty(PlatformUtils.PLATFORM_PREFIX_KEY, "Idea");
+    setIfEmpty("idea.classpath.index.enabled", "false");
+
+    VfsRootAccess.allowRootAccess(RUNFILES_PATH);
+
+    List<String> pluginJars = Lists.newArrayList();
+    try {
+      Enumeration<URL> urls = BlazeTestSystemProperties.class.getClassLoader().getResources("META-INF/plugin.xml");
+      while (urls.hasMoreElements()) {
+        URL url = urls.nextElement();
+        addArchiveFile(url, pluginJars);
+      }
+    } catch (IOException e) {
+      System.err.println("Cannot find plugin.xml resources");
+      e.printStackTrace();
+    }
+
+    setIfEmpty("idea.plugins.path", Joiner.on(File.pathSeparator).join(pluginJars));
+  }
+
+  private static void addArchiveFile(URL url, List<String> files) {
+    if ("jar".equals(url.getProtocol())) {
+      String path = url.getPath();
+      int index = path.indexOf("!/");
+      if (index > 0) {
+        String jarPath = path.substring(0, index);
+        if (jarPath.startsWith("file:")) {
+          files.add(jarPath.substring(5));
+        }
+      }
+    }
+  }
+
+  private static void setSandboxPath(String property, File path) {
+    path.mkdirs();
+    setIfEmpty(property, path.getPath());
+  }
+
+  private static void setIfEmpty(String property, String value) {
+    if (System.getProperty(property) == null) {
+      System.setProperty(property, value);
+    }
+  }
+
+  /**
+   * Gets directory that should be used for all files created during testing.
+   *
+   * <p>This method will return a directory that's common to all tests run
+   * within the same <i>build target</i>.
+   *
+   * @return standard file, for example the File representing "/tmp/zogjones/foo_unittest/".
+   */
+  private static File getTmpDirFile() {
+    File tmpDir;
+
+    // Flag value specified in environment?
+    String tmpDirStr = getUserValue("TEST_TMPDIR");
+    if ((tmpDirStr != null) && (tmpDirStr.length() > 0)) {
+      tmpDir = new File(tmpDirStr);
+    } else {
+      // Fallback default $TEMP/$USER/tmp/$TESTNAME
+      String baseTmpDir = System.getProperty("java.io.tmpdir");
+      tmpDir = new File(baseTmpDir).getAbsoluteFile();
+
+      // .. Add username
+      String username = System.getProperty("user.name");
+      username = username.replace('/', '_');
+      username = username.replace('\\', '_');
+      username = username.replace('\000', '_');
+      tmpDir = new File(tmpDir, username);
+      tmpDir = new File(tmpDir, "tmp");
+    }
+
+    // Ensure tmpDir exists
+    if (!tmpDir.isDirectory()) {
+      tmpDir.mkdirs();
+    }
+    return tmpDir;
+  }
+
+  /**
+   * Returns the value for system property <code>name</code>, or if that is
+   * not found the value of the user's environment variable <code>name</code>.
+   * If neither is found, null is returned.
+   *
+   * @param name the name of property to get
+   * @return the value of the property or null if it is not found
+   */
+  private static String getUserValue(String name) {
+    String propValue = System.getProperty(name);
+    if (propValue == null) {
+      return System.getenv(name);
+    }
+    return propValue;
+  }
+
+}
diff --git a/intellij_test/src/com/google/idea/blaze/base/suite/TestAggregator.java b/intellij_test/src/com/google/idea/blaze/base/suite/TestAggregator.java
new file mode 100644
index 0000000..af80f29
--- /dev/null
+++ b/intellij_test/src/com/google/idea/blaze/base/suite/TestAggregator.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.suite;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Used to denote a class which shouldn't be treated as a test by {@link TestClassFinder}, which
+ * searches for all test classes in the classpath matching a pattern.<br>
+ * This is useful to prevent infinite loops.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface TestAggregator {
+}
diff --git a/intellij_test/src/com/google/idea/blaze/base/suite/TestAll.java b/intellij_test/src/com/google/idea/blaze/base/suite/TestAll.java
new file mode 100644
index 0000000..fbd2a21
--- /dev/null
+++ b/intellij_test/src/com/google/idea/blaze/base/suite/TestAll.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.suite;
+
+import com.intellij.TestCaseLoader;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.util.io.FileUtil;
+import com.intellij.openapi.vfs.VfsUtilCore;
+import com.intellij.testFramework.TeamCityLogger;
+import com.intellij.testFramework.TestLoggerFactory;
+import com.intellij.testFramework.TestRunnerUtil;
+import com.intellij.util.ArrayUtil;
+
+import junit.framework.JUnit4TestAdapter;
+import junit.framework.Test;
+import junit.framework.TestCase;
+import junit.framework.TestResult;
+import junit.framework.TestSuite;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * A cut-down version of {@link com.intellij.TestAll} which supports test classes inside jars.
+ */
+@TestAggregator
+public class TestAll implements Test {
+
+  static {
+    Logger.setFactory(TestLoggerFactory.class);
+  }
+
+  private final TestCaseLoader testCaseLoader;
+
+  public TestAll(String packageRoot) throws Throwable {
+    this(packageRoot, getClassRoots());
+  }
+
+  public TestAll(String packageRoot, String... classRoots) throws IOException, ClassNotFoundException {
+    testCaseLoader = new TestCaseLoader("");
+    fillTestCases(testCaseLoader, packageRoot, classRoots);
+  }
+
+  public static String[] getClassRoots() {
+    final ClassLoader loader = TestAll.class.getClassLoader();
+    if (loader instanceof URLClassLoader) {
+      return getClassRoots(((URLClassLoader)loader).getURLs());
+    }
+    final Class<? extends ClassLoader> loaderClass = loader.getClass();
+    if (loaderClass.getName().equals("com.intellij.util.lang.UrlClassLoader")) {
+      try {
+        final Method declaredMethod = loaderClass.getDeclaredMethod("getBaseUrls");
+        final List<URL> urls = (List<URL>) declaredMethod.invoke(loader);
+        return getClassRoots(urls.toArray(new URL[urls.size()]));
+      } catch (Throwable ignore) {
+      }
+    }
+    return System.getProperty("java.class.path").split(File.pathSeparator);
+  }
+
+  private static String[] getClassRoots(URL[] urls) {
+    return Arrays.stream(urls)
+      .map(VfsUtilCore::convertFromUrl)
+      .map(VfsUtilCore::urlToPath)
+      .toArray(String[]::new);
+  }
+
+  private static boolean isIntellijPlatformJar(String classRoot) {
+    return classRoot.contains("intellij-platform-sdk");
+  }
+
+  public static void fillTestCases(TestCaseLoader testCaseLoader, String packageRoot, String... classRoots) throws IOException {
+    long before = System.currentTimeMillis();
+    for (String classRoot : classRoots) {
+      if (isIntellijPlatformJar(classRoot)) {
+        continue;
+      }
+      int oldCount = testCaseLoader.getClasses().size();
+      File classRootFile = new File(FileUtil.toSystemDependentName(classRoot));
+      Collection<String> classes = TestClassFinder.findTestClasses(classRootFile, packageRoot);
+      testCaseLoader.loadTestCases(classRootFile.getName(), classes);
+      int newCount = testCaseLoader.getClasses().size();
+      if (newCount != oldCount) {
+        System.out.println("Loaded " + (newCount - oldCount) + " tests from class root " + classRoot);
+      }
+    }
+
+    if (testCaseLoader.getClasses().size() == 1) {
+      testCaseLoader.clearClasses();
+    }
+    long after = System.currentTimeMillis();
+
+    String message = "Number of test classes found: " + testCaseLoader.getClasses().size()
+                      + " time to load: " + (after - before) / 1000 + "s.";
+    System.out.println(message);
+    log(message);
+  }
+
+  @Override
+  public int countTestCases() {
+    int count = 0;
+    for (Object aClass : testCaseLoader.getClasses()) {
+      Test test = getTest((Class)aClass);
+      if (test != null) {
+        count += test.countTestCases();
+      }
+    }
+    return count;
+  }
+
+  @Override
+  public void run(final TestResult testResult) {
+    List<Class> classes = testCaseLoader.getClasses();
+    for (Class<?> aClass : classes) {
+      runTest(testResult, aClass);
+      if (testResult.shouldStop()) {
+        break;
+      }
+    }
+  }
+
+  private void runTest(final TestResult testResult, Class testCaseClass) {
+    Test test = getTest(testCaseClass);
+    if (test == null) {
+      return;
+    }
+
+    try {
+      test.run(testResult);
+    } catch (Throwable t) {
+      testResult.addError(test, t);
+    }
+  }
+
+  @Nullable
+  private static Test getTest(final Class<?> testCaseClass) {
+    try {
+      if ((testCaseClass.getModifiers() & Modifier.PUBLIC) == 0) {
+        return null;
+      }
+      if (testCaseClass.isAnnotationPresent(TestAggregator.class)) {
+        // prevent infinite loops in 'countTestCases'
+        return null;
+      }
+
+      Method suiteMethod = safeFindMethod(testCaseClass, "suite");
+      if (suiteMethod != null) {
+        return (Test) suiteMethod.invoke(null, (Object[]) ArrayUtil.EMPTY_CLASS_ARRAY);
+      }
+
+      if (TestRunnerUtil.isJUnit4TestClass(testCaseClass)) {
+        return new JUnit4TestAdapter(testCaseClass);
+      }
+
+      final int[] testsCount = {0};
+      TestSuite suite = new TestSuite(testCaseClass) {
+        @Override
+        public void addTest(Test test) {
+          if (!(test instanceof TestCase)) {
+            doAddTest(test);
+          }
+          else {
+            String name = ((TestCase)test).getName();
+            if ("warning".equals(name)) {
+              return; // Mute TestSuite's "no tests found" warning
+            }
+            doAddTest(test);
+          }
+        }
+
+        private void doAddTest(Test test) {
+          testsCount[0]++;
+          super.addTest(test);
+        }
+      };
+
+      return testsCount[0] > 0 ? suite : null;
+    }
+    catch (Throwable t) {
+      System.err.println("Failed to load test: " + testCaseClass.getName());
+      t.printStackTrace(System.err);
+      return null;
+    }
+  }
+
+  @Nullable
+  private static Method safeFindMethod(Class<?> klass, String name) {
+    try {
+      return klass.getMethod(name);
+    }
+    catch (NoSuchMethodException e) {
+      return null;
+    }
+  }
+
+  private static void log(String message) {
+    TeamCityLogger.info(message);
+  }
+
+}
diff --git a/intellij_test/src/com/google/idea/blaze/base/suite/TestClassFinder.java b/intellij_test/src/com/google/idea/blaze/base/suite/TestClassFinder.java
new file mode 100644
index 0000000..8df1ed3
--- /dev/null
+++ b/intellij_test/src/com/google/idea/blaze/base/suite/TestClassFinder.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.suite;
+
+import com.google.common.collect.Sets;
+import com.intellij.ClassFinder;
+import com.intellij.openapi.util.text.StringUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Enumeration;
+import java.util.SortedSet;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Finds all valid test classes inside a given directory or jar.
+ */
+@TestAggregator
+public class TestClassFinder {
+
+  private static final String CLASS_EXTENSION = ".class";
+
+  /**
+   * Returns all top-level test classes underneath the specified classpath and package roots.
+   */
+  public static SortedSet<String> findTestClasses(File classRootFile, String packageRoot) throws IOException {
+    if (isJar(classRootFile.getPath())) {
+      return findTestClassesInJar(classRootFile, packageRoot);
+    }
+    ClassFinder finder = new ClassFinder(classRootFile, packageRoot, true);
+    return Sets.newTreeSet(finder.getClasses());
+  }
+
+  private static SortedSet<String> findTestClassesInJar(File classPathRoot, String packageRoot) throws IOException {
+    packageRoot = packageRoot.replace('.', File.separatorChar);
+    SortedSet<String> classNames = Sets.newTreeSet();
+    ZipFile zipFile = new ZipFile(classPathRoot.getPath());
+    if (!packageRoot.isEmpty() && zipFile.getEntry(packageRoot) == null) {
+      return Sets.newTreeSet();
+    }
+    Enumeration<? extends ZipEntry> entries = zipFile.entries();
+    while (entries.hasMoreElements()) {
+      String entryName = entries.nextElement().getName();
+      if (entryName.endsWith(CLASS_EXTENSION) && isTopLevelClass(entryName) && entryName.startsWith(packageRoot)) {
+        classNames.add(getClassName(entryName));
+      }
+    }
+    return classNames;
+  }
+
+  private static boolean isJar(String filePath) {
+    return filePath.endsWith(".jar");
+  }
+
+  private static boolean isTopLevelClass(String fileName) {
+    return fileName.indexOf('$') < 0;
+  }
+
+  /**
+   * Given the absolute path of a class file, return the class name.
+   */
+  private static String getClassName(String className) {
+    return StringUtil.trimEnd(className, CLASS_EXTENSION).replace(File.separatorChar, '.');
+  }
+}
diff --git a/intellij_test/src/com/google/idea/blaze/base/suite/TestSuiteBuilder.java b/intellij_test/src/com/google/idea/blaze/base/suite/TestSuiteBuilder.java
new file mode 100644
index 0000000..78c384c
--- /dev/null
+++ b/intellij_test/src/com/google/idea/blaze/base/suite/TestSuiteBuilder.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 The Bazel Authors. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.idea.blaze.base.suite;
+
+import com.google.common.base.Strings;
+import com.google.idea.blaze.base.BlazeTestSystemProperties;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+@TestAggregator
+public class TestSuiteBuilder {
+  public static Test suite() throws Throwable {
+
+    BlazeTestSystemProperties.configureSystemProperties();
+
+    String packageRoot = System.getProperty("idea.test.package.root");
+    packageRoot = Strings.nullToEmpty(packageRoot);
+
+    TestSuite suite = new TestSuite();
+    suite.addTest(new TestAll(packageRoot));
+    return suite;
+  }
+
+}
diff --git a/intellij_test/test_defs.bzl b/intellij_test/test_defs.bzl
new file mode 100644
index 0000000..72029a8
--- /dev/null
+++ b/intellij_test/test_defs.bzl
@@ -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.
+
+"""Custom rule for creating IntelliJ plugin tests.
+"""
+
+# The JVM flags common to all test rules
+JVM_FLAGS_FOR_TESTS = [
+    "-Didea.classpath.index.enabled=false",
+    "-Djava.awt.headless=true",
+]
+
+def intellij_test(name,
+                  srcs,
+                  test_package_root,
+                  deps,
+                  platform_prefix="Idea",
+                  required_plugins=None,
+                  integration_tests=False):
+  """Creates a java_test rule comprising all valid test classes
+  in the specified srcs.
+
+  Args:
+    name: name of this rule.
+    srcs: the test classes.
+    test_package_root: only tests under this package root will be run.
+    deps: the required deps.
+    plugin_jar: a target building the plugin to be tested. This will be added to the classpath.
+    platform_prefix: Specifies the JetBrains product these tests are run against. Examples are
+        'Idea' (IJ CE), 'idea' (IJ UE), 'CLion', 'AndroidStudio'. See
+        com.intellij.util.PlatformUtils for other options.
+    required_plugins: optional comma-separated list of plugin IDs. Integration tests will fail if
+        these plugins aren't loaded at runtime.
+    integration_tests: if true, bundled IJ core plugins will be added to the classpath.
+  """
+
+  if integration_tests:
+    deps.append("//intellij-platform-sdk:bundled_plugins")
+
+
+  jvm_flags = JVM_FLAGS_FOR_TESTS + [
+      "-Didea.platform.prefix=" + platform_prefix,
+      "-Didea.test.package.root=" + test_package_root,
+  ]
+
+  if required_plugins:
+    jvm_flags.append("-Didea.required.plugins.id=" + required_plugins)
+
+  native.java_test(
+      name = name,
+      srcs = srcs,
+      deps = deps,
+      size = "medium" if integration_tests else "small",
+      jvm_flags = jvm_flags,
+      test_class = "com.google.idea.blaze.base.suite.TestSuiteBuilder",
+  )
\ No newline at end of file
diff --git a/remote_platform_sdks/BUILD.android_studio b/remote_platform_sdks/BUILD.android_studio
new file mode 100644
index 0000000..3e394c2
--- /dev/null
+++ b/remote_platform_sdks/BUILD.android_studio
@@ -0,0 +1,36 @@
+# Description:
+#
+# Plugin source jars for Android Studio 2.2. Preview 4, accessed remotely.
+
+package(default_visibility = ["//visibility:public"])
+
+java_import(
+    name = "plugin_api",
+    jars = glob([
+        "android-studio/lib/*.jar",
+        "android-studio/plugins/android/lib/*.jar",
+        "android-studio/plugins/android-ndk/lib/*.jar",
+    ]),
+    tags = ["intellij-provided-by-sdk"],
+)
+
+# The plugins required by ASwB. We need to include them
+# when running integration tests.
+java_import(
+    name = "bundled_plugins",
+    jars = glob([
+        "android-studio/plugins/android/lib/*.jar",
+        "android-studio/plugins/android-ndk/lib/*.jar",
+        "android-studio/plugins/gradle/lib/*.jar",
+        "android-studio/plugins/Groovy/lib/*.jar",
+        "android-studio/plugins/java-i18n/lib/*.jar",
+        "android-studio/plugins/junit/lib/*.jar",
+        "android-studio/plugins/properties/lib/*.jar",
+    ]),
+    tags = ["intellij-provided-by-sdk"],
+)
+
+filegroup(
+    name = "build_number",
+    srcs = ["android-studio/build.txt"],
+)
diff --git a/remote_platform_sdks/BUILD.clion b/remote_platform_sdks/BUILD.clion
new file mode 100644
index 0000000..48593a8
--- /dev/null
+++ b/remote_platform_sdks/BUILD.clion
@@ -0,0 +1,24 @@
+# Description:
+#
+# Plugin source jars for CLion 2016.1.3, accessed remotely.
+
+package(default_visibility = ["//visibility:public"])
+
+java_import(
+    name = "plugin_api",
+    jars = glob(["clion-2016.1.3/lib/*.jar"]),
+    tags = ["intellij-provided-by-sdk"],
+)
+
+filegroup(
+    name = "build_number",
+    srcs = ["clion-2016.1.3/build.txt"],
+)
+
+# The plugins required by CLwB. Presumably there will be some, when we write
+# some integration tests.
+java_import(
+    name = "bundled_plugins",
+    jars = [],
+    tags = ["intellij-provided-by-sdk"],
+)
diff --git a/remote_platform_sdks/BUILD.idea b/remote_platform_sdks/BUILD.idea
new file mode 100644
index 0000000..c8106a5
--- /dev/null
+++ b/remote_platform_sdks/BUILD.idea
@@ -0,0 +1,35 @@
+# Description:
+#
+# Plugin source jars for IntelliJ 2016.1.3, accessed remotely.
+
+package(default_visibility = ["//visibility:public"])
+
+java_import(
+    name = "plugin_api",
+    jars = glob(["idea-IC-145.*/lib/*.jar"]),
+    tags = ["intellij-provided-by-sdk"],
+)
+
+java_import(
+    name = "devkit",
+    jars = glob(["idea-IC-145.*/plugins/devkit/lib/devkit.jar"]),
+    tags = ["intellij-provided-by-sdk"],
+)
+
+# The plugins required by IJwB. We need to include them
+# when running integration tests.
+java_import(
+    name = "bundled_plugins",
+    jars = glob([
+        "idea-IC-145.*/plugins/devkit/lib/*.jar",
+        "idea-IC-145.*/plugins/java-i18n/lib/*.jar",
+        "idea-IC-145.*/plugins/junit/lib/*.jar",
+        "idea-IC-145.*/plugins/properties/lib/*.jar",
+    ]),
+    tags = ["intellij-provided-by-sdk"],
+)
+
+filegroup(
+    name = "build_number",
+    srcs = glob(["idea-IC-145.*/build.txt"]),
+)
diff --git a/third_party/BUILD b/third_party/BUILD
new file mode 100644
index 0000000..37a3284
--- /dev/null
+++ b/third_party/BUILD
@@ -0,0 +1,22 @@
+package(default_visibility = ["//visibility:public"])
+
+java_import(
+    name = "trickle",
+    jars = ["trickle/trickle-0.6.1.jar"],
+)
+
+java_import(
+    name = "jsr305",
+    jars = ["jsr305/jsr305.jar"],
+)
+
+java_import(
+    name = "test_lib",
+    jars = [
+        "jdk8/tools.jar",
+        "truth/truth.jar",
+        "mockito/mockito-core-1.9.5.jar",
+        "junit4/junit-4.12.jar",
+        "objenesis/objenesis-1_3.jar",
+    ],
+)
diff --git a/third_party/README.md b/third_party/README.md
new file mode 100644
index 0000000..0cceaa6
--- /dev/null
+++ b/third_party/README.md
@@ -0,0 +1,39 @@
+This file lists license and version information of all code we did not
+author, but ship together with the source so building IJwB requires
+a minimal set of extra dependencies.
+
+
+## [trickle](https://github.com/spotify/trickle/archive/trickle-0.6.1.zip)
+
+* Version: 0.6.1
+* License: Apache License 2.0
+
+## [mockito](http://mockito.googlecode.com/files/mockito-1.9.5.zip)
+
+* Version: 1.9.5
+* License: MIT
+
+## [jsr-305](http://central.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.1/jsr305-3.0.1.jar)
+
+* Version: 3.0.1
+* License: BSD License
+
+## [junit4](https://github.com/junit-team/junit/archive/r4.12.zip)
+
+* Version: 4.12
+* License: Eclipse Public License 1.0
+
+## [objenesis](https://objenesis.googlecode.com/files/objenesis-1.3-bin.zip)
+
+* Version: 1.3
+* License: Apache 2.0
+
+## [java jdk8](http://hg.openjdk.java.net/jdk8u/jdk8u60)
+
+* Version: jdk8u60-b27 (rev)
+* License: GPLv2 and GPLv2+ClassPath Exception
+
+## [truth](https://github.com/google/truth)
+
+* Version: 0.28
+* License: Apache 2.0
diff --git a/third_party/jdk8/LICENSE b/third_party/jdk8/LICENSE
new file mode 100644
index 0000000..b40a0f4
--- /dev/null
+++ b/third_party/jdk8/LICENSE
@@ -0,0 +1,347 @@
+The GNU General Public License (GPL)
+
+Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Everyone is permitted to copy and distribute verbatim copies of this license
+document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to share
+and change it.  By contrast, the GNU General Public License is intended to
+guarantee your freedom to share and change free software--to make sure the
+software is free for all its users.  This General Public License applies to
+most of the Free Software Foundation's software and to any other program whose
+authors commit to using it.  (Some other Free Software Foundation software is
+covered by the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+When we speak of free software, we are referring to freedom, not price.  Our
+General Public Licenses are designed to make sure that you have the freedom to
+distribute copies of free software (and charge for this service if you wish),
+that you receive source code or can get it if you want it, that you can change
+the software or use pieces of it in new free programs; and that you know you
+can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone to deny
+you these rights or to ask you to surrender the rights.  These restrictions
+translate to certain responsibilities for you if you distribute copies of the
+software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis or for
+a fee, you must give the recipients all the rights that you have.  You must
+make sure that they, too, receive or can get the source code.  And you must
+show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and (2)
+offer you this license which gives you legal permission to copy, distribute
+and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain that
+everyone understands that there is no warranty for this free software.  If the
+software is modified by someone else and passed on, we want its recipients to
+know that what they have is not the original, so that any problems introduced
+by others will not reflect on the original authors' reputations.
+
+Finally, any free program is threatened constantly by software patents.  We
+wish to avoid the danger that redistributors of a free program will
+individually obtain patent licenses, in effect making the program proprietary.
+To prevent this, we have made it clear that any patent must be licensed for
+everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and modification
+follow.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License applies to any program or other work which contains a notice
+placed by the copyright holder saying it may be distributed under the terms of
+this General Public License.  The "Program", below, refers to any such program
+or work, and a "work based on the Program" means either the Program or any
+derivative work under copyright law: that is to say, a work containing the
+Program or a portion of it, either verbatim or with modifications and/or
+translated into another language.  (Hereinafter, translation is included
+without limitation in the term "modification".) Each licensee is addressed as
+"you".
+
+Activities other than copying, distribution and modification are not covered by
+this License; they are outside its scope.  The act of running the Program is
+not restricted, and the output from the Program is covered only if its contents
+constitute a work based on the Program (independent of having been made by
+running the Program).  Whether that is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source code as
+you receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice and
+disclaimer of warranty; keep intact all the notices that refer to this License
+and to the absence of any warranty; and give any other recipients of the
+Program a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and you may
+at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of it, thus
+forming a work based on the Program, and copy and distribute such modifications
+or work under the terms of Section 1 above, provided that you also meet all of
+these conditions:
+
+    a) You must cause the modified files to carry prominent notices stating
+    that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in whole or
+    in part contains or is derived from the Program or any part thereof, to be
+    licensed as a whole at no charge to all third parties under the terms of
+    this License.
+
+    c) If the modified program normally reads commands interactively when run,
+    you must cause it, when started running for such interactive use in the
+    most ordinary way, to print or display an announcement including an
+    appropriate copyright notice and a notice that there is no warranty (or
+    else, saying that you provide a warranty) and that users may redistribute
+    the program under these conditions, and telling the user how to view a copy
+    of this License.  (Exception: if the Program itself is interactive but does
+    not normally print such an announcement, your work based on the Program is
+    not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If identifiable
+sections of that work are not derived from the Program, and can be reasonably
+considered independent and separate works in themselves, then this License, and
+its terms, do not apply to those sections when you distribute them as separate
+works.  But when you distribute the same sections as part of a whole which is a
+work based on the Program, the distribution of the whole must be on the terms
+of this License, whose permissions for other licensees extend to the entire
+whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest your
+rights to work written entirely by you; rather, the intent is to exercise the
+right to control the distribution of derivative or collective works based on
+the Program.
+
+In addition, mere aggregation of another work not based on the Program with the
+Program (or with a work based on the Program) on a volume of a storage or
+distribution medium does not bring the other work under the scope of this
+License.
+
+3. You may copy and distribute the Program (or a work based on it, under
+Section 2) in object code or executable form under the terms of Sections 1 and
+2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable source
+    code, which must be distributed under the terms of Sections 1 and 2 above
+    on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three years, to
+    give any third party, for a charge no more than your cost of physically
+    performing source distribution, a complete machine-readable copy of the
+    corresponding source code, to be distributed under the terms of Sections 1
+    and 2 above on a medium customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer to
+    distribute corresponding source code.  (This alternative is allowed only
+    for noncommercial distribution and only if you received the program in
+    object code or executable form with such an offer, in accord with
+    Subsection b above.)
+
+The source code for a work means the preferred form of the work for making
+modifications to it.  For an executable work, complete source code means all
+the source code for all modules it contains, plus any associated interface
+definition files, plus the scripts used to control compilation and installation
+of the executable.  However, as a special exception, the source code
+distributed need not include anything that is normally distributed (in either
+source or binary form) with the major components (compiler, kernel, and so on)
+of the operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the source
+code from the same place counts as distribution of the source code, even though
+third parties are not compelled to copy the source along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program except as
+expressly provided under this License.  Any attempt otherwise to copy, modify,
+sublicense or distribute the Program is void, and will automatically terminate
+your rights under this License.  However, parties who have received copies, or
+rights, from you under this License will not have their licenses terminated so
+long as such parties remain in full compliance.
+
+5. You are not required to accept this License, since you have not signed it.
+However, nothing else grants you permission to modify or distribute the Program
+or its derivative works.  These actions are prohibited by law if you do not
+accept this License.  Therefore, by modifying or distributing the Program (or
+any work based on the Program), you indicate your acceptance of this License to
+do so, and all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the Program),
+the recipient automatically receives a license from the original licensor to
+copy, distribute or modify the Program subject to these terms and conditions.
+You may not impose any further restrictions on the recipients' exercise of the
+rights granted herein.  You are not responsible for enforcing compliance by
+third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues), conditions
+are imposed on you (whether by court order, agreement or otherwise) that
+contradict the conditions of this License, they do not excuse you from the
+conditions of this License.  If you cannot distribute so as to satisfy
+simultaneously your obligations under this License and any other pertinent
+obligations, then as a consequence you may not distribute the Program at all.
+For example, if a patent license would not permit royalty-free redistribution
+of the Program by all those who receive copies directly or indirectly through
+you, then the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply and
+the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any patents or
+other property right claims or to contest validity of any such claims; this
+section has the sole purpose of protecting the integrity of the free software
+distribution system, which is implemented by public license practices.  Many
+people have made generous contributions to the wide range of software
+distributed through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing to
+distribute software through any other system and a licensee cannot impose that
+choice.
+
+This section is intended to make thoroughly clear what is believed to be a
+consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in certain
+countries either by patents or by copyrighted interfaces, the original
+copyright holder who places the Program under this License may add an explicit
+geographical distribution limitation excluding those countries, so that
+distribution is permitted only in or among countries not thus excluded.  In
+such case, this License incorporates the limitation as if written in the body
+of this License.
+
+9. The Free Software Foundation may publish revised and/or new versions of the
+General Public License from time to time.  Such new versions will be similar in
+spirit to the present version, but may differ in detail to address new problems
+or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any later
+version", you have the option of following the terms and conditions either of
+that version or of any later version published by the Free Software Foundation.
+If the Program does not specify a version number of this License, you may
+choose any version ever published by the Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free programs
+whose distribution conditions are different, write to the author to ask for
+permission.  For software which is copyrighted by the Free Software Foundation,
+write to the Free Software Foundation; we sometimes make exceptions for this.
+Our decision will be guided by the two goals of preserving the free status of
+all derivatives of our free software and of promoting the sharing and reuse of
+software generally.
+
+NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR
+THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN OTHERWISE
+STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE
+PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
+INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE,
+YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL
+ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE
+PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR
+INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA
+BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER
+OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest possible
+use to the public, the best way to achieve this is to make it free software
+which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program.  It is safest to attach
+them to the start of each source file to most effectively convey the exclusion
+of warranty; and each file should have at least the "copyright" line and a
+pointer to where the full notice is found.
+
+    One line to give the program's name and a brief idea of what it does.
+
+    Copyright (C) <year> <name of author>
+
+    This program is free software; you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by the Free
+    Software Foundation; either version 2 of the License, or (at your option)
+    any later version.
+
+    This program is distributed in the hope that it will be useful, but WITHOUT
+    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+    more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc., 59
+    Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this when it
+starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author Gnomovision comes
+    with ABSOLUTELY NO WARRANTY; for details type 'show w'.  This is free
+    software, and you are welcome to redistribute it under certain conditions;
+    type 'show c' for details.
+
+The hypothetical commands 'show w' and 'show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may be
+called something other than 'show w' and 'show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.  Here
+is a sample; alter the names:
+
+    Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+    'Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+    signature of Ty Coon, 1 April 1989
+
+    Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General Public
+License instead of this License.
+
+
+"CLASSPATH" EXCEPTION TO THE GPL
+
+Certain source files distributed by Oracle America and/or its affiliates are
+subject to the following clarification and special exception to the GPL, but
+only where Oracle has expressly included in the particular source file's header
+the words "Oracle designates this particular file as subject to the "Classpath"
+exception as provided by Oracle in the LICENSE file that accompanied this code."
+
+    Linking this library statically or dynamically with other modules is making
+    a combined work based on this library.  Thus, the terms and conditions of
+    the GNU General Public License cover the whole combination.
+
+    As a special exception, the copyright holders of this library give you
+    permission to link this library with independent modules to produce an
+    executable, regardless of the license terms of these independent modules,
+    and to copy and distribute the resulting executable under terms of your
+    choice, provided that you also meet, for each linked independent module,
+    the terms and conditions of the license of that module.  An independent
+    module is a module which is not derived from or based on this library.  If
+    you modify this library, you may extend this exception to your version of
+    the library, but you are not obligated to do so.  If you do not wish to do
+    so, delete this exception statement from your version.
diff --git a/third_party/jdk8/tools.jar b/third_party/jdk8/tools.jar
new file mode 100644
index 0000000..1e18b3e
--- /dev/null
+++ b/third_party/jdk8/tools.jar
Binary files differ
diff --git a/third_party/jsr305/LICENSE b/third_party/jsr305/LICENSE
new file mode 100644
index 0000000..6736681
--- /dev/null
+++ b/third_party/jsr305/LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2007-2009, JSR305 expert group
+All rights reserved.
+
+http://www.opensource.org/licenses/bsd-license.php
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice, 
+      this list of conditions and the following disclaimer in the documentation 
+      and/or other materials provided with the distribution.
+    * Neither the name of the JSR305 expert group nor the names of its 
+      contributors may be used to endorse or promote products derived from 
+      this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/third_party/jsr305/jsr305.jar b/third_party/jsr305/jsr305.jar
new file mode 100644
index 0000000..f973aea
--- /dev/null
+++ b/third_party/jsr305/jsr305.jar
Binary files differ
diff --git a/third_party/junit4/LICENSE b/third_party/junit4/LICENSE
new file mode 100644
index 0000000..fb68629
--- /dev/null
+++ b/third_party/junit4/LICENSE
@@ -0,0 +1,214 @@
+JUnit
+
+Eclipse Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
+LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
+CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+      a) in the case of the initial Contributor, the initial code and
+         documentation distributed under this Agreement, and
+      b) in the case of each subsequent Contributor:
+
+      i) changes to the Program, and
+
+      ii) additions to the Program;
+
+      where such changes and/or additions to the Program originate from and are
+distributed by that particular Contributor. A Contribution 'originates' from a
+Contributor if it was added to the Program by such Contributor itself or anyone
+acting on such Contributor's behalf. Contributions do not include additions to
+the Program which: (i) are separate modules of software distributed in
+conjunction with the Program under their own license agreement, and (ii) are
+not derivative works of the Program. 
+
+"Contributor" means any person or entity that distributes the Program.
+
+"Licensed Patents " mean patent claims licensable by a Contributor which are
+necessarily infringed by the use or sale of its Contribution alone or when
+combined with the Program.
+
+"Program" means the Contributions distributed in accordance with this Agreement.
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+2. GRANT OF RIGHTS
+
+      a) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free copyright license to
+reproduce, prepare derivative works of, publicly display, publicly perform,
+distribute and sublicense the Contribution of such Contributor, if any, and
+such derivative works, in source code and object code form.
+
+      b) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free patent license under
+Licensed Patents to make, use, sell, offer to sell, import and otherwise
+transfer the Contribution of such Contributor, if any, in source code and
+object code form. This patent license shall apply to the combination of the
+Contribution and the Program if, at the time the Contribution is added by the
+Contributor, such addition of the Contribution causes such combination to be
+covered by the Licensed Patents. The patent license shall not apply to any
+other combinations which include the Contribution. No hardware per se is
+licensed hereunder. 
+
+      c) Recipient understands that although each Contributor grants the
+licenses to its Contributions set forth herein, no assurances are provided by
+any Contributor that the Program does not infringe the patent or other
+intellectual property rights of any other entity. Each Contributor disclaims
+any liability to Recipient for claims brought by any other entity based on
+infringement of intellectual property rights or otherwise. As a condition to
+exercising the rights and licenses granted hereunder, each Recipient hereby
+assumes sole responsibility to secure any other intellectual property rights
+needed, if any. For example, if a third party patent license is required to
+allow Recipient to distribute the Program, it is Recipient's responsibility to
+acquire that license before distributing the Program.
+
+      d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright license
+set forth in this Agreement. 
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under
+its own license agreement, provided that:
+
+      a) it complies with the terms and conditions of this Agreement; and
+
+      b) its license agreement:
+
+      i) effectively disclaims on behalf of all Contributors all warranties and
+conditions, express and implied, including warranties or conditions of title
+and non-infringement, and implied warranties or conditions of merchantability
+and fitness for a particular purpose; 
+
+      ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and consequential
+damages, such as lost profits; 
+
+      iii) states that any provisions which differ from this Agreement are
+offered by that Contributor alone and not by any other party; and
+
+      iv) states that source code for the Program is available from such
+Contributor, and informs licensees how to obtain it in a reasonable manner on
+or through a medium customarily used for software exchange. 
+
+When the Program is made available in source code form:
+
+      a) it must be made available under this Agreement; and 
+
+      b) a copy of this Agreement must be included with each copy of the
+Program. 
+
+Contributors may not remove or alter any copyright notices contained within the
+Program.
+
+Each Contributor must identify itself as the originator of its Contribution, if
+any, in a manner that reasonably allows subsequent Recipients to identify the
+originator of the Contribution.
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with
+respect to end users, business partners and the like. While this license is
+intended to facilitate the commercial use of the Program, the Contributor who
+includes the Program in a commercial product offering should do so in a manner
+which does not create potential liability for other Contributors. Therefore, if
+a Contributor includes the Program in a commercial product offering, such
+Contributor ("Commercial Contributor") hereby agrees to defend and indemnify
+every other Contributor ("Indemnified Contributor") against any losses, damages
+and costs (collectively "Losses") arising from claims, lawsuits and other legal
+actions brought by a third party against the Indemnified Contributor to the
+extent caused by the acts or omissions of such Commercial Contributor in
+connection with its distribution of the Program in a commercial product
+offering. The obligations in this section do not apply to any claims or Losses
+relating to any actual or alleged intellectual property infringement. In order
+to qualify, an Indemnified Contributor must: a) promptly notify the Commercial
+Contributor in writing of such claim, and b) allow the Commercial Contributor
+to control, and cooperate with the Commercial Contributor in, the defense and
+any related settlement negotiations. The Indemnified Contributor may
+participate in any such claim at its own expense.
+
+For example, a Contributor might include the Program in a commercial product
+offering, Product X. That Contributor is then a Commercial Contributor. If that
+Commercial Contributor then makes performance claims, or offers warranties
+related to Product X, those performance claims and warranties are such
+Commercial Contributor's responsibility alone. Under this section, the
+Commercial Contributor would have to defend claims against the other
+Contributors related to those performance claims and warranties, and if a court
+requires any other Contributor to pay any damages as a result, the Commercial
+Contributor must pay those damages.
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
+Recipient is solely responsible for determining the appropriateness of using
+and distributing the Program and assumes all risks associated with its exercise
+of rights under this Agreement, including but not limited to the risks and
+costs of program errors, compliance with applicable laws, damage to or loss of
+data, programs or equipment, and unavailability or interruption of operations.
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
+CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST
+PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
+WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS
+GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable
+law, it shall not affect the validity or enforceability of the remainder of the
+terms of this Agreement, and without further action by the parties hereto, such
+provision shall be reformed to the minimum extent necessary to make such
+provision valid and enforceable.
+
+If Recipient institutes patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging that the
+Program itself (excluding combinations of the Program with other software or
+hardware) infringes such Recipient's patent(s), then such Recipient's rights
+granted under Section 2(b) shall terminate as of the date such litigation is
+filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to
+comply with any of the material terms or conditions of this Agreement and does
+not cure such failure in a reasonable period of time after becoming aware of
+such noncompliance. If all Recipient's rights under this Agreement terminate,
+Recipient agrees to cease use and distribution of the Program as soon as
+reasonably practicable. However, Recipient's obligations under this Agreement
+and any licenses granted by Recipient relating to the Program shall continue
+and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in
+order to avoid inconsistency the Agreement is copyrighted and may only be
+modified in the following manner. The Agreement Steward reserves the right to
+publish new versions (including revisions) of this Agreement from time to time.
+No one other than the Agreement Steward has the right to modify this Agreement.
+The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to
+serve as the Agreement Steward to a suitable separate entity. Each new version
+of the Agreement will be given a distinguishing version number. The Program
+(including Contributions) may always be distributed subject to the version of
+the Agreement under which it was received. In addition, after a new version of
+the Agreement is published, Contributor may elect to distribute the Program
+(including its Contributions) under the new version. Except as expressly stated
+in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to
+the intellectual property of any Contributor under this Agreement, whether
+expressly, by implication, estoppel or otherwise. All rights in the Program not
+expressly granted under this Agreement are reserved.
+
+This Agreement is governed by the laws of the State of New York and the
+intellectual property laws of the United States of America. No party to this
+Agreement will bring a legal action under this Agreement more than one year
+after the cause of action arose. Each party waives its rights to a jury trial
+in any resulting litigation. 
+
diff --git a/third_party/junit4/junit-4.12.jar b/third_party/junit4/junit-4.12.jar
new file mode 100644
index 0000000..3a7fc26
--- /dev/null
+++ b/third_party/junit4/junit-4.12.jar
Binary files differ
diff --git a/third_party/mockito/LICENSE b/third_party/mockito/LICENSE
new file mode 100644
index 0000000..e0840a4
--- /dev/null
+++ b/third_party/mockito/LICENSE
@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2007 Mockito contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/third_party/mockito/mockito-core-1.9.5.jar b/third_party/mockito/mockito-core-1.9.5.jar
new file mode 100644
index 0000000..5de7610
--- /dev/null
+++ b/third_party/mockito/mockito-core-1.9.5.jar
Binary files differ
diff --git a/third_party/objenesis/LICENSE b/third_party/objenesis/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/third_party/objenesis/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/third_party/objenesis/objenesis-1_3.jar b/third_party/objenesis/objenesis-1_3.jar
new file mode 100644
index 0000000..d56dc2b
--- /dev/null
+++ b/third_party/objenesis/objenesis-1_3.jar
Binary files differ
diff --git a/third_party/trickle/LICENSE b/third_party/trickle/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/third_party/trickle/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/third_party/trickle/trickle-0.6.1.jar b/third_party/trickle/trickle-0.6.1.jar
new file mode 100644
index 0000000..bb0965e
--- /dev/null
+++ b/third_party/trickle/trickle-0.6.1.jar
Binary files differ
diff --git a/third_party/truth/LICENSE b/third_party/truth/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/third_party/truth/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/third_party/truth/truth.jar b/third_party/truth/truth.jar
new file mode 100755
index 0000000..e0a5a6e
--- /dev/null
+++ b/third_party/truth/truth.jar
Binary files differ
diff --git a/tools/zip/BUILD b/tools/zip/BUILD
new file mode 100644
index 0000000..13c4d21
--- /dev/null
+++ b/tools/zip/BUILD
@@ -0,0 +1,6 @@
+package(default_visibility = ["//visibility:public"])
+
+sh_binary(
+    name = "zip",
+    srcs = ["zip.sh"],
+)
diff --git a/tools/zip/zip.sh b/tools/zip/zip.sh
new file mode 100755
index 0000000..daed8f6
--- /dev/null
+++ b/tools/zip/zip.sh
@@ -0,0 +1,15 @@
+# Copyright 2015 The Bazel Authors. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+zip "$@"