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 "$@"