summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Thorogood <thorogood@google.com>2016-02-19 15:29:32 +1100
committerSam Thorogood <thorogood@google.com>2016-02-19 15:29:32 +1100
commitdb6ed59e8576fe47e72c870bd66e4d6e9e016899 (patch)
tree18333bfca4ef57ab045c364427535a254906d7d1
parent1df0580bda965544c9d8258a6dd252fa279e13e3 (diff)
downloadsamples-db6ed59e8576fe47e72c870bd66e4d6e9e016899.tar.gz
Initial checkin
Change-Id: Id923dacba7e3e7193b90542be25ce540e824f00d
-rw-r--r--google-samples.iml48
-rw-r--r--src/META-INF/plugin.xml38
-rw-r--r--src/com/appspot/cluestick_server/search/README2
-rw-r--r--src/com/appspot/cluestick_server/search/Search.java535
-rw-r--r--src/com/appspot/cluestick_server/search/SearchRequest.java208
-rw-r--r--src/com/appspot/cluestick_server/search/SearchRequestInitializer.java121
-rw-r--r--src/com/appspot/cluestick_server/search/model/CodeResult.java149
-rw-r--r--src/com/appspot/cluestick_server/search/model/EventReq.java128
-rw-r--r--src/com/appspot/cluestick_server/search/model/Result.java128
-rw-r--r--src/com/appspot/cluestick_server/search/model/ResultContext.java107
-rw-r--r--src/com/appspot/cluestick_server/search/model/SearchResponse.java107
-rw-r--r--src/com/google/devrel/cluestick/searchservice/CluestickSearch.java108
-rw-r--r--src/com/google/devrel/cluestick/searchservice/EventLog.java124
-rw-r--r--src/com/google/devrel/cluestick/studioclient/Browser.java47
-rw-r--r--src/com/google/devrel/cluestick/studioclient/CodeBrowser.java305
-rw-r--r--src/com/google/devrel/cluestick/studioclient/DynamicToolWindowWrapper.java87
-rw-r--r--src/com/google/devrel/cluestick/studioclient/FindSampleUsageAction.java189
-rw-r--r--src/com/google/devrel/cluestick/studioclient/IconFetcher.java34
-rw-r--r--src/com/google/devrel/cluestick/studioclient/PsiHelpers.java294
-rw-r--r--src/com/google/devrel/cluestick/studioclient/ResultUtils.java75
-rw-r--r--src/com/google/devrel/cluestick/studioclient/SearchResultsNode.java39
-rw-r--r--src/com/google/devrel/cluestick/studioclient/SearchResultsTree.java289
-rw-r--r--src/com/google/devrel/cluestick/studioclient/SearchResultsView.java300
-rw-r--r--src/com/google/devrel/cluestick/studioclient/Symbol.java66
-rw-r--r--src/resources/com/google/devrel/cluestick/studioclient/developers.pngbin0 -> 627 bytes
-rw-r--r--src/resources/com/google/devrel/cluestick/studioclient/developers_2x.pngbin0 -> 1244 bytes
26 files changed, 3528 insertions, 0 deletions
diff --git a/google-samples.iml b/google-samples.iml
new file mode 100644
index 0000000..8cd1539
--- /dev/null
+++ b/google-samples.iml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
+ <exclude-output />
+ <content url="file://$MODULE_DIR$">
+ <sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
+ <sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
+ <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" name="Guava" level="project" />
+ <orderEntry type="module" module-name="android" />
+ <orderEntry type="module" module-name="extensions" />
+ <orderEntry type="module" module-name="annotations" />
+ <orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
+ <orderEntry type="library" scope="TEST" name="fest" level="project" />
+ <orderEntry type="module" module-name="platform-api" />
+ <orderEntry type="module" module-name="google-login-as" />
+ <orderEntry type="module" module-name="core-api" />
+ <orderEntry type="module" module-name="editor-ui-api" />
+ <orderEntry type="module" module-name="analysis-api" />
+ <orderEntry type="module" module-name="indexing-api" />
+ <orderEntry type="module" module-name="xml-psi-api" />
+ <orderEntry type="module" module-name="projectModel-api" />
+ <orderEntry type="module" module-name="lang-impl" />
+ <orderEntry type="module" module-name="lang-api" />
+ <orderEntry type="module" module-name="dom-openapi" />
+ <orderEntry type="module" module-name="java-psi-api" />
+ <orderEntry type="module" module-name="java-psi-impl" />
+ <orderEntry type="module" module-name="groovy-psi" />
+ <orderEntry type="library" name="google-api-java-client" level="project" />
+ <orderEntry type="module-library">
+ <library>
+ <CLASSES>
+ <root url="jar://$MODULE_DIR$/lib/libcluestick_java.jar!/" />
+ </CLASSES>
+ <JAVADOC />
+ <SOURCES />
+ </library>
+ </orderEntry>
+ <orderEntry type="module" module-name="java-analysis-impl" />
+ <orderEntry type="module" module-name="openapi" />
+ <orderEntry type="module" module-name="java-impl" />
+ <orderEntry type="module" module-name="testFramework-java" />
+ <orderEntry type="library" scope="TEST" name="mockito" level="project" />
+ </component>
+</module>
diff --git a/src/META-INF/plugin.xml b/src/META-INF/plugin.xml
new file mode 100644
index 0000000..2f0d42d
--- /dev/null
+++ b/src/META-INF/plugin.xml
@@ -0,0 +1,38 @@
+<idea-plugin version="2">
+ <id>com.google.cluestick.studioclient</id>
+ <name>Google Developers Samples</name>
+ <version>0.3.0</version>
+ <vendor url="https://developers.google.com">Google Developers</vendor>
+
+ <!-- TODO(thorogood): These provide e.g. NetHttpTransport -->
+ <depends>org.jetbrains.android</depends>
+ <depends>com.google.gct.login</depends>
+
+ <description><![CDATA[
+ <h1>Google Developers Samples</h1>
+ <p>
+ Allows searching Google's code samples and other resources based on the active symbol.
+ </p>
+ ]]></description>
+
+ <idea-version since-build="131"/>
+
+ <extensions defaultExtensionNs="com.intellij">
+ <applicationService serviceImplementation="com.google.devrel.cluestick.searchservice.CluestickSearch"
+ serviceInterface="com.google.devrel.cluestick.searchservice.CluestickSearch" />
+ <projectService serviceImplementation="com.google.devrel.cluestick.studioclient.DynamicToolWindowWrapper"/>
+ </extensions>
+
+ <application-components>
+ </application-components>
+
+ <project-components>
+ </project-components>
+
+ <actions>
+ <action id="Cluestick.SearchMenu" class="com.google.devrel.cluestick.studioclient.FindSampleUsageAction" text="_Find Sample Code">
+ <add-to-group group-id="EditorPopupMenu1.FindRefactor" anchor="after" relative-to-action="FindUsages" />
+ <keyboard-shortcut keymap="$default" first-keystroke="alt F8" />
+ </action>
+ </actions>
+</idea-plugin>
diff --git a/src/com/appspot/cluestick_server/search/README b/src/com/appspot/cluestick_server/search/README
new file mode 100644
index 0000000..70ce2a6
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/README
@@ -0,0 +1,2 @@
+This is the generated client library for the Find Samples backend, required to
+be compiled here for 1.6 classfile compatibility.
diff --git a/src/com/appspot/cluestick_server/search/Search.java b/src/com/appspot/cluestick_server/search/Search.java
new file mode 100644
index 0000000..855648e
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/Search.java
@@ -0,0 +1,535 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * This code was generated by https://github.com/google/apis-client-generator/
+ * (build: 2016-01-08 17:48:37 UTC)
+ * on 2016-02-17 at 11:17:28 UTC
+ * Modify at your own risk.
+ */
+
+package com.appspot.cluestick_server.search;
+
+/**
+ * Service definition for Search (v1).
+ *
+ * <p>
+ * Google Sample Code Index & API
+ * </p>
+ *
+ * <p>
+ * For more information about this service, see the
+ * <a href="" target="_blank">API Documentation</a>
+ * </p>
+ *
+ * <p>
+ * This service uses {@link SearchRequestInitializer} to initialize global parameters via its
+ * {@link Builder}.
+ * </p>
+ *
+ * @since 1.3
+ * @author Google, Inc.
+ */
+@SuppressWarnings("javadoc")
+public class Search extends com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient {
+
+ // Note: Leave this static initializer at the top of the file.
+ static {
+ com.google.api.client.util.Preconditions.checkState(
+ com.google.api.client.googleapis.GoogleUtils.MAJOR_VERSION == 1 &&
+ com.google.api.client.googleapis.GoogleUtils.MINOR_VERSION >= 15,
+ "You are currently running with version %s of google-api-client. " +
+ "You need at least version 1.15 of google-api-client to run version " +
+ "1.21.0 of the search library.", com.google.api.client.googleapis.GoogleUtils.VERSION);
+ }
+
+ /**
+ * The default encoded root URL of the service. This is determined when the library is generated
+ * and normally should not be changed.
+ *
+ * @since 1.7
+ */
+ public static final String DEFAULT_ROOT_URL = "https://cluestick-server.appspot.com/_ah/api/";
+
+ /**
+ * The default encoded service path of the service. This is determined when the library is
+ * generated and normally should not be changed.
+ *
+ * @since 1.7
+ */
+ public static final String DEFAULT_SERVICE_PATH = "search/v1/";
+
+ /**
+ * The default encoded base URL of the service. This is determined when the library is generated
+ * and normally should not be changed.
+ */
+ public static final String DEFAULT_BASE_URL = DEFAULT_ROOT_URL + DEFAULT_SERVICE_PATH;
+
+ /**
+ * Constructor.
+ *
+ * <p>
+ * Use {@link Builder} if you need to specify any of the optional parameters.
+ * </p>
+ *
+ * @param transport HTTP transport, which should normally be:
+ * <ul>
+ * <li>Google App Engine:
+ * {@code com.google.api.client.extensions.appengine.http.UrlFetchTransport}</li>
+ * <li>Android: {@code newCompatibleTransport} from
+ * {@code com.google.api.client.extensions.android.http.AndroidHttp}</li>
+ * <li>Java: {@link com.google.api.client.googleapis.javanet.GoogleNetHttpTransport#newTrustedTransport()}
+ * </li>
+ * </ul>
+ * @param jsonFactory JSON factory, which may be:
+ * <ul>
+ * <li>Jackson: {@code com.google.api.client.json.jackson2.JacksonFactory}</li>
+ * <li>Google GSON: {@code com.google.api.client.json.gson.GsonFactory}</li>
+ * <li>Android Honeycomb or higher:
+ * {@code com.google.api.client.extensions.android.json.AndroidJsonFactory}</li>
+ * </ul>
+ * @param httpRequestInitializer HTTP request initializer or {@code null} for none
+ * @since 1.7
+ */
+ public Search(com.google.api.client.http.HttpTransport transport, com.google.api.client.json.JsonFactory jsonFactory,
+ com.google.api.client.http.HttpRequestInitializer httpRequestInitializer) {
+ this(new Builder(transport, jsonFactory, httpRequestInitializer));
+ }
+
+ /**
+ * @param builder builder
+ */
+ Search(Builder builder) {
+ super(builder);
+ }
+
+ @Override
+ protected void initialize(com.google.api.client.googleapis.services.AbstractGoogleClientRequest<?> httpClientRequest) throws java.io.IOException {
+ super.initialize(httpClientRequest);
+ }
+
+ /**
+ * Create a request for the method "event".
+ *
+ * This request holds the parameters needed by the search server. After setting any optional
+ * parameters, call the {@link Event#execute()} method to invoke the remote operation.
+ *
+ * @param content the {@link com.appspot.cluestick_server.search.model.EventReq}
+ * @return the request
+ */
+ public Event event(com.appspot.cluestick_server.search.model.EventReq content) throws java.io.IOException {
+ Event result = new Event(content);
+ initialize(result);
+ return result;
+ }
+
+ public class Event extends SearchRequest<Void> {
+
+ private static final String REST_PATH = "event";
+
+ /**
+ * Create a request for the method "event".
+ *
+ * This request holds the parameters needed by the the search server. After setting any optional
+ * parameters, call the {@link Event#execute()} method to invoke the remote operation. <p> {@link
+ * Event#initialize(com.google.api.client.googleapis.services.AbstractGoogleClientRequest)} must
+ * be called to initialize this instance immediately after invoking the constructor. </p>
+ *
+ * @param content the {@link com.appspot.cluestick_server.search.model.EventReq}
+ * @since 1.13
+ */
+ protected Event(com.appspot.cluestick_server.search.model.EventReq content) {
+ super(Search.this, "POST", REST_PATH, content, Void.class);
+ }
+
+ @Override
+ public Event setAlt(java.lang.String alt) {
+ return (Event) super.setAlt(alt);
+ }
+
+ @Override
+ public Event setFields(java.lang.String fields) {
+ return (Event) super.setFields(fields);
+ }
+
+ @Override
+ public Event setKey(java.lang.String key) {
+ return (Event) super.setKey(key);
+ }
+
+ @Override
+ public Event setOauthToken(java.lang.String oauthToken) {
+ return (Event) super.setOauthToken(oauthToken);
+ }
+
+ @Override
+ public Event setPrettyPrint(java.lang.Boolean prettyPrint) {
+ return (Event) super.setPrettyPrint(prettyPrint);
+ }
+
+ @Override
+ public Event setQuotaUser(java.lang.String quotaUser) {
+ return (Event) super.setQuotaUser(quotaUser);
+ }
+
+ @Override
+ public Event setUserIp(java.lang.String userIp) {
+ return (Event) super.setUserIp(userIp);
+ }
+
+ @Override
+ public Event set(String parameterName, Object value) {
+ return (Event) super.set(parameterName, value);
+ }
+ }
+
+ /**
+ * Search the Index
+ *
+ * Create a request for the method "search".
+ *
+ * This request holds the parameters needed by the search server. After setting any optional
+ * parameters, call the {@link SearchOperation#execute()} method to invoke the remote operation.
+ *
+ * @return the request
+ */
+ public SearchOperation search() throws java.io.IOException {
+ SearchOperation result = new SearchOperation();
+ initialize(result);
+ return result;
+ }
+
+ public class SearchOperation extends SearchRequest<com.appspot.cluestick_server.search.model.SearchResponse> {
+
+ private static final String REST_PATH = "search";
+
+ /**
+ * Search the Index
+ *
+ * Create a request for the method "search".
+ *
+ * This request holds the parameters needed by the the search server. After setting any optional
+ * parameters, call the {@link SearchOperation#execute()} method to invoke the remote operation.
+ * <p> {@link SearchOperation#initialize(com.google.api.client.googleapis.services.AbstractGoogleC
+ * lientRequest)} must be called to initialize this instance immediately after invoking the
+ * constructor. </p>
+ *
+ * @since 1.13
+ */
+ protected SearchOperation() {
+ super(Search.this, "GET", REST_PATH, null, com.appspot.cluestick_server.search.model.SearchResponse.class);
+ }
+
+ @Override
+ public com.google.api.client.http.HttpResponse executeUsingHead() throws java.io.IOException {
+ return super.executeUsingHead();
+ }
+
+ @Override
+ public com.google.api.client.http.HttpRequest buildHttpRequestUsingHead() throws java.io.IOException {
+ return super.buildHttpRequestUsingHead();
+ }
+
+ @Override
+ public SearchOperation setAlt(java.lang.String alt) {
+ return (SearchOperation) super.setAlt(alt);
+ }
+
+ @Override
+ public SearchOperation setFields(java.lang.String fields) {
+ return (SearchOperation) super.setFields(fields);
+ }
+
+ @Override
+ public SearchOperation setKey(java.lang.String key) {
+ return (SearchOperation) super.setKey(key);
+ }
+
+ @Override
+ public SearchOperation setOauthToken(java.lang.String oauthToken) {
+ return (SearchOperation) super.setOauthToken(oauthToken);
+ }
+
+ @Override
+ public SearchOperation setPrettyPrint(java.lang.Boolean prettyPrint) {
+ return (SearchOperation) super.setPrettyPrint(prettyPrint);
+ }
+
+ @Override
+ public SearchOperation setQuotaUser(java.lang.String quotaUser) {
+ return (SearchOperation) super.setQuotaUser(quotaUser);
+ }
+
+ @Override
+ public SearchOperation setUserIp(java.lang.String userIp) {
+ return (SearchOperation) super.setUserIp(userIp);
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.String lang;
+
+ /**
+
+ */
+ public java.lang.String getLang() {
+ return lang;
+ }
+
+ public SearchOperation setLang(java.lang.String lang) {
+ this.lang = lang;
+ return this;
+ }
+
+ @com.google.api.client.util.Key("package")
+ private java.lang.String package__;
+
+ /**
+
+ */
+ public java.lang.String getPackage() {
+ return package__;
+ }
+
+ public SearchOperation setPackage(java.lang.String package__) {
+ this.package__ = package__;
+ return this;
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.String symbol;
+
+ /**
+
+ */
+ public java.lang.String getSymbol() {
+ return symbol;
+ }
+
+ public SearchOperation setSymbol(java.lang.String symbol) {
+ this.symbol = symbol;
+ return this;
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.Boolean prefill;
+
+ /**
+
+ */
+ public java.lang.Boolean getPrefill() {
+ return prefill;
+ }
+
+ public SearchOperation setPrefill(java.lang.Boolean prefill) {
+ this.prefill = prefill;
+ return this;
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.Integer limit;
+
+ /**
+ [ default: 10]
+ [
+
+ */
+ public java.lang.Integer getLimit() {
+ return limit;
+ }
+
+ public SearchOperation setLimit(java.lang.Integer limit) {
+ this.limit = limit;
+ return this;
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.String sessionId;
+
+ /**
+
+ */
+ public java.lang.String getSessionId() {
+ return sessionId;
+ }
+
+ public SearchOperation setSessionId(java.lang.String sessionId) {
+ this.sessionId = sessionId;
+ return this;
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.Boolean hasIndex;
+
+ /**
+
+ */
+ public java.lang.Boolean getHasIndex() {
+ return hasIndex;
+ }
+
+ public SearchOperation setHasIndex(java.lang.Boolean hasIndex) {
+ this.hasIndex = hasIndex;
+ return this;
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.String env;
+
+ /**
+
+ */
+ public java.lang.String getEnv() {
+ return env;
+ }
+
+ public SearchOperation setEnv(java.lang.String env) {
+ this.env = env;
+ return this;
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.String userAgent;
+
+ /**
+
+ */
+ public java.lang.String getUserAgent() {
+ return userAgent;
+ }
+
+ public SearchOperation setUserAgent(java.lang.String userAgent) {
+ this.userAgent = userAgent;
+ return this;
+ }
+
+ @com.google.api.client.util.Key
+ private java.lang.Boolean allContext;
+
+ /**
+
+ */
+ public java.lang.Boolean getAllContext() {
+ return allContext;
+ }
+
+ public SearchOperation setAllContext(java.lang.Boolean allContext) {
+ this.allContext = allContext;
+ return this;
+ }
+
+ @Override
+ public SearchOperation set(String parameterName, Object value) {
+ return (SearchOperation) super.set(parameterName, value);
+ }
+ }
+
+ /**
+ * Builder for {@link Search}.
+ *
+ * <p>
+ * Implementation is not thread-safe.
+ * </p>
+ *
+ * @since 1.3.0
+ */
+ public static final class Builder extends com.google.api.client.googleapis.services.json.AbstractGoogleJsonClient.Builder {
+
+ /**
+ * Returns an instance of a new builder.
+ *
+ * @param transport HTTP transport, which should normally be:
+ * <ul>
+ * <li>Google App Engine:
+ * {@code com.google.api.client.extensions.appengine.http.UrlFetchTransport}</li>
+ * <li>Android: {@code newCompatibleTransport} from
+ * {@code com.google.api.client.extensions.android.http.AndroidHttp}</li>
+ * <li>Java: {@link com.google.api.client.googleapis.javanet.GoogleNetHttpTransport#newTrustedTransport()}
+ * </li>
+ * </ul>
+ * @param jsonFactory JSON factory, which may be:
+ * <ul>
+ * <li>Jackson: {@code com.google.api.client.json.jackson2.JacksonFactory}</li>
+ * <li>Google GSON: {@code com.google.api.client.json.gson.GsonFactory}</li>
+ * <li>Android Honeycomb or higher:
+ * {@code com.google.api.client.extensions.android.json.AndroidJsonFactory}</li>
+ * </ul>
+ * @param httpRequestInitializer HTTP request initializer or {@code null} for none
+ * @since 1.7
+ */
+ public Builder(com.google.api.client.http.HttpTransport transport, com.google.api.client.json.JsonFactory jsonFactory,
+ com.google.api.client.http.HttpRequestInitializer httpRequestInitializer) {
+ super(
+ transport,
+ jsonFactory,
+ DEFAULT_ROOT_URL,
+ DEFAULT_SERVICE_PATH,
+ httpRequestInitializer,
+ false);
+ }
+
+ /** Builds a new instance of {@link Search}. */
+ @Override
+ public Search build() {
+ return new Search(this);
+ }
+
+ @Override
+ public Builder setRootUrl(String rootUrl) {
+ return (Builder) super.setRootUrl(rootUrl);
+ }
+
+ @Override
+ public Builder setServicePath(String servicePath) {
+ return (Builder) super.setServicePath(servicePath);
+ }
+
+ @Override
+ public Builder setHttpRequestInitializer(com.google.api.client.http.HttpRequestInitializer httpRequestInitializer) {
+ return (Builder) super.setHttpRequestInitializer(httpRequestInitializer);
+ }
+
+ @Override
+ public Builder setApplicationName(String applicationName) {
+ return (Builder) super.setApplicationName(applicationName);
+ }
+
+ @Override
+ public Builder setSuppressPatternChecks(boolean suppressPatternChecks) {
+ return (Builder) super.setSuppressPatternChecks(suppressPatternChecks);
+ }
+
+ @Override
+ public Builder setSuppressRequiredParameterChecks(boolean suppressRequiredParameterChecks) {
+ return (Builder) super.setSuppressRequiredParameterChecks(suppressRequiredParameterChecks);
+ }
+
+ @Override
+ public Builder setSuppressAllChecks(boolean suppressAllChecks) {
+ return (Builder) super.setSuppressAllChecks(suppressAllChecks);
+ }
+
+ /**
+ * Set the {@link SearchRequestInitializer}.
+ *
+ * @since 1.12
+ */
+ public Builder setSearchRequestInitializer(
+ SearchRequestInitializer searchRequestInitializer) {
+ return (Builder) super.setGoogleClientRequestInitializer(searchRequestInitializer);
+ }
+
+ @Override
+ public Builder setGoogleClientRequestInitializer(
+ com.google.api.client.googleapis.services.GoogleClientRequestInitializer googleClientRequestInitializer) {
+ return (Builder) super.setGoogleClientRequestInitializer(googleClientRequestInitializer);
+ }
+ }
+}
diff --git a/src/com/appspot/cluestick_server/search/SearchRequest.java b/src/com/appspot/cluestick_server/search/SearchRequest.java
new file mode 100644
index 0000000..171757e
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/SearchRequest.java
@@ -0,0 +1,208 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * This code was generated by https://github.com/google/apis-client-generator/
+ * (build: 2016-01-08 17:48:37 UTC)
+ * on 2016-02-17 at 11:17:28 UTC
+ * Modify at your own risk.
+ */
+
+package com.appspot.cluestick_server.search;
+
+/**
+ * Search request.
+ *
+ * @since 1.3
+ */
+@SuppressWarnings("javadoc")
+public abstract class SearchRequest<T> extends com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest<T> {
+
+ /**
+ * @param client Google client
+ * @param method HTTP Method
+ * @param uriTemplate URI template for the path relative to the base URL. If it starts with a "/"
+ * the base path from the base URL will be stripped out. The URI template can also be a
+ * full URL. URI template expansion is done using
+ * {@link com.google.api.client.http.UriTemplate#expand(String, String, Object, boolean)}
+ * @param content A POJO that can be serialized into JSON or {@code null} for none
+ * @param responseClass response class to parse into
+ */
+ public SearchRequest(
+ Search client, String method, String uriTemplate, Object content, Class<T> responseClass) {
+ super(
+ client,
+ method,
+ uriTemplate,
+ content,
+ responseClass);
+ }
+
+ /** Data format for the response. */
+ @com.google.api.client.util.Key
+ private java.lang.String alt;
+
+ /**
+ * Data format for the response. [default: json]
+ */
+ public java.lang.String getAlt() {
+ return alt;
+ }
+
+ /** Data format for the response. */
+ public SearchRequest<T> setAlt(java.lang.String alt) {
+ this.alt = alt;
+ return this;
+ }
+
+ /** Selector specifying which fields to include in a partial response. */
+ @com.google.api.client.util.Key
+ private java.lang.String fields;
+
+ /**
+ * Selector specifying which fields to include in a partial response.
+ */
+ public java.lang.String getFields() {
+ return fields;
+ }
+
+ /** Selector specifying which fields to include in a partial response. */
+ public SearchRequest<T> setFields(java.lang.String fields) {
+ this.fields = fields;
+ return this;
+ }
+
+ /**
+ * API key. Your API key identifies your project and provides you with API access, quota, and
+ * reports. Required unless you provide an OAuth 2.0 token.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String key;
+
+ /**
+ * API key. Your API key identifies your project and provides you with API access, quota, and
+ * reports. Required unless you provide an OAuth 2.0 token.
+ */
+ public java.lang.String getKey() {
+ return key;
+ }
+
+ /**
+ * API key. Your API key identifies your project and provides you with API access, quota, and
+ * reports. Required unless you provide an OAuth 2.0 token.
+ */
+ public SearchRequest<T> setKey(java.lang.String key) {
+ this.key = key;
+ return this;
+ }
+
+ /** OAuth 2.0 token for the current user. */
+ @com.google.api.client.util.Key("oauth_token")
+ private java.lang.String oauthToken;
+
+ /**
+ * OAuth 2.0 token for the current user.
+ */
+ public java.lang.String getOauthToken() {
+ return oauthToken;
+ }
+
+ /** OAuth 2.0 token for the current user. */
+ public SearchRequest<T> setOauthToken(java.lang.String oauthToken) {
+ this.oauthToken = oauthToken;
+ return this;
+ }
+
+ /** Returns response with indentations and line breaks. */
+ @com.google.api.client.util.Key
+ private java.lang.Boolean prettyPrint;
+
+ /**
+ * Returns response with indentations and line breaks. [default: true]
+ */
+ public java.lang.Boolean getPrettyPrint() {
+ return prettyPrint;
+ }
+
+ /** Returns response with indentations and line breaks. */
+ public SearchRequest<T> setPrettyPrint(java.lang.Boolean prettyPrint) {
+ this.prettyPrint = prettyPrint;
+ return this;
+ }
+
+ /**
+ * Available to use for quota purposes for server-side applications. Can be any arbitrary string
+ * assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String quotaUser;
+
+ /**
+ * Available to use for quota purposes for server-side applications. Can be any arbitrary string
+ * assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.
+ */
+ public java.lang.String getQuotaUser() {
+ return quotaUser;
+ }
+
+ /**
+ * Available to use for quota purposes for server-side applications. Can be any arbitrary string
+ * assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.
+ */
+ public SearchRequest<T> setQuotaUser(java.lang.String quotaUser) {
+ this.quotaUser = quotaUser;
+ return this;
+ }
+
+ /**
+ * IP address of the site where the request originates. Use this if you want to enforce per-user
+ * limits.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String userIp;
+
+ /**
+ * IP address of the site where the request originates. Use this if you want to enforce per-user
+ * limits.
+ */
+ public java.lang.String getUserIp() {
+ return userIp;
+ }
+
+ /**
+ * IP address of the site where the request originates. Use this if you want to enforce per-user
+ * limits.
+ */
+ public SearchRequest<T> setUserIp(java.lang.String userIp) {
+ this.userIp = userIp;
+ return this;
+ }
+
+ @Override
+ public final Search getAbstractGoogleClient() {
+ return (Search) super.getAbstractGoogleClient();
+ }
+
+ @Override
+ public SearchRequest<T> setDisableGZipContent(boolean disableGZipContent) {
+ return (SearchRequest<T>) super.setDisableGZipContent(disableGZipContent);
+ }
+
+ @Override
+ public SearchRequest<T> setRequestHeaders(com.google.api.client.http.HttpHeaders headers) {
+ return (SearchRequest<T>) super.setRequestHeaders(headers);
+ }
+
+ @Override
+ public SearchRequest<T> set(String parameterName, Object value) {
+ return (SearchRequest<T>) super.set(parameterName, value);
+ }
+}
diff --git a/src/com/appspot/cluestick_server/search/SearchRequestInitializer.java b/src/com/appspot/cluestick_server/search/SearchRequestInitializer.java
new file mode 100644
index 0000000..d7cff6e
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/SearchRequestInitializer.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * This code was generated by https://github.com/google/apis-client-generator/
+ * (build: 2016-01-08 17:48:37 UTC)
+ * on 2016-02-17 at 11:17:28 UTC
+ * Modify at your own risk.
+ */
+
+package com.appspot.cluestick_server.search;
+
+/**
+ * Search request initializer for setting properties like key and userIp.
+ *
+ * <p>
+ * The simplest usage is to use it to set the key parameter:
+ * </p>
+ *
+ * <pre>
+ public static final GoogleClientRequestInitializer KEY_INITIALIZER =
+ new SearchRequestInitializer(KEY);
+ * </pre>
+ *
+ * <p>
+ * There is also a constructor to set both the key and userIp parameters:
+ * </p>
+ *
+ * <pre>
+ public static final GoogleClientRequestInitializer INITIALIZER =
+ new SearchRequestInitializer(KEY, USER_IP);
+ * </pre>
+ *
+ * <p>
+ * If you want to implement custom logic, extend it like this:
+ * </p>
+ *
+ * <pre>
+ public static class MyRequestInitializer extends SearchRequestInitializer {
+
+ {@literal @}Override
+ public void initializeSearchRequest(SearchRequest{@literal <}?{@literal >} request)
+ throws IOException {
+ // custom logic
+ }
+ }
+ * </pre>
+ *
+ * <p>
+ * Finally, to set the key and userIp parameters and insert custom logic, extend it like this:
+ * </p>
+ *
+ * <pre>
+ public static class MyRequestInitializer2 extends SearchRequestInitializer {
+
+ public MyKeyRequestInitializer() {
+ super(KEY, USER_IP);
+ }
+
+ {@literal @}Override
+ public void initializeSearchRequest(SearchRequest{@literal <}?{@literal >} request)
+ throws IOException {
+ // custom logic
+ }
+ }
+ * </pre>
+ *
+ * <p>
+ * Subclasses should be thread-safe.
+ * </p>
+ *
+ * @since 1.12
+ */
+public class SearchRequestInitializer extends com.google.api.client.googleapis.services.json.CommonGoogleJsonClientRequestInitializer {
+
+ public SearchRequestInitializer() {
+ super();
+ }
+
+ /**
+ * @param key API key or {@code null} to leave it unchanged
+ */
+ public SearchRequestInitializer(String key) {
+ super(key);
+ }
+
+ /**
+ * @param key API key or {@code null} to leave it unchanged
+ * @param userIp user IP or {@code null} to leave it unchanged
+ */
+ public SearchRequestInitializer(String key, String userIp) {
+ super(key, userIp);
+ }
+
+ @Override
+ public final void initializeJsonRequest(com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest<?> request) throws java.io.IOException {
+ super.initializeJsonRequest(request);
+ initializeSearchRequest((SearchRequest<?>) request);
+ }
+
+ /**
+ * Initializes Search request.
+ *
+ * <p>
+ * Default implementation does nothing. Called from
+ * {@link #initializeJsonRequest(com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest)}.
+ * </p>
+ *
+ * @throws java.io.IOException I/O exception
+ */
+ protected void initializeSearchRequest(SearchRequest<?> request) throws java.io.IOException {
+ }
+}
diff --git a/src/com/appspot/cluestick_server/search/model/CodeResult.java b/src/com/appspot/cluestick_server/search/model/CodeResult.java
new file mode 100644
index 0000000..3f78ab1
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/model/CodeResult.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * This code was generated by https://github.com/google/apis-client-generator/
+ * (build: 2016-01-08 17:48:37 UTC)
+ * on 2016-02-17 at 11:17:28 UTC
+ * Modify at your own risk.
+ */
+
+package com.appspot.cluestick_server.search.model;
+
+/**
+ * Model definition for CodeResult.
+ *
+ * <p> This is the Java data model class that specifies how to parse/serialize into the JSON that is
+ * transmitted over HTTP when working with the search. For a detailed explanation see:
+ * <a href="https://developers.google.com/api-client-library/java/google-http-java-client/json">https://developers.google.com/api-client-library/java/google-http-java-client/json</a>
+ * </p>
+ *
+ * @author Google, Inc.
+ */
+@SuppressWarnings("javadoc")
+public final class CodeResult extends com.google.api.client.json.GenericJson {
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String branch;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.util.List<ResultContext> context;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.util.List<java.lang.Integer> lines;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String path;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String repo;
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getBranch() {
+ return branch;
+ }
+
+ /**
+ * @param branch branch or {@code null} for none
+ */
+ public CodeResult setBranch(java.lang.String branch) {
+ this.branch = branch;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.util.List<ResultContext> getContext() {
+ return context;
+ }
+
+ /**
+ * @param context context or {@code null} for none
+ */
+ public CodeResult setContext(java.util.List<ResultContext> context) {
+ this.context = context;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.util.List<java.lang.Integer> getLines() {
+ return lines;
+ }
+
+ /**
+ * @param lines lines or {@code null} for none
+ */
+ public CodeResult setLines(java.util.List<java.lang.Integer> lines) {
+ this.lines = lines;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getPath() {
+ return path;
+ }
+
+ /**
+ * @param path path or {@code null} for none
+ */
+ public CodeResult setPath(java.lang.String path) {
+ this.path = path;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getRepo() {
+ return repo;
+ }
+
+ /**
+ * @param repo repo or {@code null} for none
+ */
+ public CodeResult setRepo(java.lang.String repo) {
+ this.repo = repo;
+ return this;
+ }
+
+ @Override
+ public CodeResult set(String fieldName, Object value) {
+ return (CodeResult) super.set(fieldName, value);
+ }
+
+ @Override
+ public CodeResult clone() {
+ return (CodeResult) super.clone();
+ }
+
+}
diff --git a/src/com/appspot/cluestick_server/search/model/EventReq.java b/src/com/appspot/cluestick_server/search/model/EventReq.java
new file mode 100644
index 0000000..fef0073
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/model/EventReq.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * This code was generated by https://github.com/google/apis-client-generator/
+ * (build: 2016-01-08 17:48:37 UTC)
+ * on 2016-02-17 at 11:17:28 UTC
+ * Modify at your own risk.
+ */
+
+package com.appspot.cluestick_server.search.model;
+
+/**
+ * Model definition for EventReq.
+ *
+ * <p> This is the Java data model class that specifies how to parse/serialize into the JSON that is
+ * transmitted over HTTP when working with the search. For a detailed explanation see:
+ * <a href="https://developers.google.com/api-client-library/java/google-http-java-client/json">https://developers.google.com/api-client-library/java/google-http-java-client/json</a>
+ * </p>
+ *
+ * @author Google, Inc.
+ */
+@SuppressWarnings("javadoc")
+public final class EventReq extends com.google.api.client.json.GenericJson {
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String action;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String resultKey;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String sessionId;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.Integer signal;
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getAction() {
+ return action;
+ }
+
+ /**
+ * @param action action or {@code null} for none
+ */
+ public EventReq setAction(java.lang.String action) {
+ this.action = action;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getResultKey() {
+ return resultKey;
+ }
+
+ /**
+ * @param resultKey resultKey or {@code null} for none
+ */
+ public EventReq setResultKey(java.lang.String resultKey) {
+ this.resultKey = resultKey;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getSessionId() {
+ return sessionId;
+ }
+
+ /**
+ * @param sessionId sessionId or {@code null} for none
+ */
+ public EventReq setSessionId(java.lang.String sessionId) {
+ this.sessionId = sessionId;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.Integer getSignal() {
+ return signal;
+ }
+
+ /**
+ * @param signal signal or {@code null} for none
+ */
+ public EventReq setSignal(java.lang.Integer signal) {
+ this.signal = signal;
+ return this;
+ }
+
+ @Override
+ public EventReq set(String fieldName, Object value) {
+ return (EventReq) super.set(fieldName, value);
+ }
+
+ @Override
+ public EventReq clone() {
+ return (EventReq) super.clone();
+ }
+
+}
diff --git a/src/com/appspot/cluestick_server/search/model/Result.java b/src/com/appspot/cluestick_server/search/model/Result.java
new file mode 100644
index 0000000..07ed0a7
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/model/Result.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * This code was generated by https://github.com/google/apis-client-generator/
+ * (build: 2016-01-08 17:48:37 UTC)
+ * on 2016-02-17 at 11:17:28 UTC
+ * Modify at your own risk.
+ */
+
+package com.appspot.cluestick_server.search.model;
+
+/**
+ * Model definition for Result.
+ *
+ * <p> This is the Java data model class that specifies how to parse/serialize into the JSON that is
+ * transmitted over HTTP when working with the search. For a detailed explanation see:
+ * <a href="https://developers.google.com/api-client-library/java/google-http-java-client/json">https://developers.google.com/api-client-library/java/google-http-java-client/json</a>
+ * </p>
+ *
+ * @author Google, Inc.
+ */
+@SuppressWarnings("javadoc")
+public final class Result extends com.google.api.client.json.GenericJson {
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private CodeResult code;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String key;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String text;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String url;
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public CodeResult getCode() {
+ return code;
+ }
+
+ /**
+ * @param code code or {@code null} for none
+ */
+ public Result setCode(CodeResult code) {
+ this.code = code;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getKey() {
+ return key;
+ }
+
+ /**
+ * @param key key or {@code null} for none
+ */
+ public Result setKey(java.lang.String key) {
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getText() {
+ return text;
+ }
+
+ /**
+ * @param text text or {@code null} for none
+ */
+ public Result setText(java.lang.String text) {
+ this.text = text;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getUrl() {
+ return url;
+ }
+
+ /**
+ * @param url url or {@code null} for none
+ */
+ public Result setUrl(java.lang.String url) {
+ this.url = url;
+ return this;
+ }
+
+ @Override
+ public Result set(String fieldName, Object value) {
+ return (Result) super.set(fieldName, value);
+ }
+
+ @Override
+ public Result clone() {
+ return (Result) super.clone();
+ }
+
+}
diff --git a/src/com/appspot/cluestick_server/search/model/ResultContext.java b/src/com/appspot/cluestick_server/search/model/ResultContext.java
new file mode 100644
index 0000000..88f19fb
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/model/ResultContext.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * This code was generated by https://github.com/google/apis-client-generator/
+ * (build: 2016-01-08 17:48:37 UTC)
+ * on 2016-02-17 at 11:17:28 UTC
+ * Modify at your own risk.
+ */
+
+package com.appspot.cluestick_server.search.model;
+
+/**
+ * Model definition for ResultContext.
+ *
+ * <p> This is the Java data model class that specifies how to parse/serialize into the JSON that is
+ * transmitted over HTTP when working with the search. For a detailed explanation see:
+ * <a href="https://developers.google.com/api-client-library/java/google-http-java-client/json">https://developers.google.com/api-client-library/java/google-http-java-client/json</a>
+ * </p>
+ *
+ * @author Google, Inc.
+ */
+@SuppressWarnings("javadoc")
+public final class ResultContext extends com.google.api.client.json.GenericJson {
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.util.List<java.lang.Integer> resultsAt;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.util.List<java.lang.String> src;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.Integer startAt;
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.util.List<java.lang.Integer> getResultsAt() {
+ return resultsAt;
+ }
+
+ /**
+ * @param resultsAt resultsAt or {@code null} for none
+ */
+ public ResultContext setResultsAt(java.util.List<java.lang.Integer> resultsAt) {
+ this.resultsAt = resultsAt;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.util.List<java.lang.String> getSrc() {
+ return src;
+ }
+
+ /**
+ * @param src src or {@code null} for none
+ */
+ public ResultContext setSrc(java.util.List<java.lang.String> src) {
+ this.src = src;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.Integer getStartAt() {
+ return startAt;
+ }
+
+ /**
+ * @param startAt startAt or {@code null} for none
+ */
+ public ResultContext setStartAt(java.lang.Integer startAt) {
+ this.startAt = startAt;
+ return this;
+ }
+
+ @Override
+ public ResultContext set(String fieldName, Object value) {
+ return (ResultContext) super.set(fieldName, value);
+ }
+
+ @Override
+ public ResultContext clone() {
+ return (ResultContext) super.clone();
+ }
+
+}
diff --git a/src/com/appspot/cluestick_server/search/model/SearchResponse.java b/src/com/appspot/cluestick_server/search/model/SearchResponse.java
new file mode 100644
index 0000000..184e5bd
--- /dev/null
+++ b/src/com/appspot/cluestick_server/search/model/SearchResponse.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
+ * in compliance with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+/*
+ * This code was generated by https://github.com/google/apis-client-generator/
+ * (build: 2016-01-08 17:48:37 UTC)
+ * on 2016-02-17 at 11:17:28 UTC
+ * Modify at your own risk.
+ */
+
+package com.appspot.cluestick_server.search.model;
+
+/**
+ * Model definition for SearchResponse.
+ *
+ * <p> This is the Java data model class that specifies how to parse/serialize into the JSON that is
+ * transmitted over HTTP when working with the search. For a detailed explanation see:
+ * <a href="https://developers.google.com/api-client-library/java/google-http-java-client/json">https://developers.google.com/api-client-library/java/google-http-java-client/json</a>
+ * </p>
+ *
+ * @author Google, Inc.
+ */
+@SuppressWarnings("javadoc")
+public final class SearchResponse extends com.google.api.client.json.GenericJson {
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.Integer count;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.util.List<Result> results;
+
+ /**
+ * The value may be {@code null}.
+ */
+ @com.google.api.client.util.Key
+ private java.lang.String sessionId;
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.Integer getCount() {
+ return count;
+ }
+
+ /**
+ * @param count count or {@code null} for none
+ */
+ public SearchResponse setCount(java.lang.Integer count) {
+ this.count = count;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.util.List<Result> getResults() {
+ return results;
+ }
+
+ /**
+ * @param results results or {@code null} for none
+ */
+ public SearchResponse setResults(java.util.List<Result> results) {
+ this.results = results;
+ return this;
+ }
+
+ /**
+ * @return value or {@code null} for none
+ */
+ public java.lang.String getSessionId() {
+ return sessionId;
+ }
+
+ /**
+ * @param sessionId sessionId or {@code null} for none
+ */
+ public SearchResponse setSessionId(java.lang.String sessionId) {
+ this.sessionId = sessionId;
+ return this;
+ }
+
+ @Override
+ public SearchResponse set(String fieldName, Object value) {
+ return (SearchResponse) super.set(fieldName, value);
+ }
+
+ @Override
+ public SearchResponse clone() {
+ return (SearchResponse) super.clone();
+ }
+
+}
diff --git a/src/com/google/devrel/cluestick/searchservice/CluestickSearch.java b/src/com/google/devrel/cluestick/searchservice/CluestickSearch.java
new file mode 100644
index 0000000..cbfdaba
--- /dev/null
+++ b/src/com/google/devrel/cluestick/searchservice/CluestickSearch.java
@@ -0,0 +1,108 @@
+/*
+ * 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.devrel.cluestick.searchservice;
+
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.jackson2.JacksonFactory;
+import com.google.devrel.cluestick.studioclient.Symbol;
+
+import com.appspot.cluestick_server.search.Search;
+import com.appspot.cluestick_server.search.model.EventReq;
+import com.appspot.cluestick_server.search.model.Result;
+import com.appspot.cluestick_server.search.model.SearchResponse;
+import com.intellij.openapi.diagnostic.Logger;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Plugin service to make search queries to Cluestick server.
+ */
+public class CluestickSearch {
+ private static final Logger LOG =
+ Logger.getInstance("#com.google.devrel.cluestick.searchservice.CluestickSearch");
+ public static final String DEFAULT_USER_AGENT = "cluestick search";
+
+ private final Search searchService;
+ private String sessionId;
+
+ public CluestickSearch() {
+ searchService = new Search(new NetHttpTransport(), new JacksonFactory(), null);
+ }
+
+ public CluestickSearch(HttpTransport transport, JsonFactory jsonFactory) {
+ searchService = new Search(transport, jsonFactory, null);
+ }
+
+ /**
+ * Logs an event to the Cluestick service.
+ *
+ * @param action The string action, e.g. "copy".
+ * @param resultKey The result being operated on, if any.
+ * @param signal Either a positive or negative signal, e.g. for code result quality.
+ */
+ public void logEvent(@Nullable String action, @Nullable String resultKey, int signal) {
+ EventReq event = new EventReq();
+ event.setAction(action);
+ event.setResultKey(resultKey);
+ event.setSignal(signal);
+ event.setSignal(event.getSignal().intValue() + signal);
+ try {
+ LOG.info("Logging event: " + event);
+ Search.Event request = searchService.event(event);
+ request.execute(); // no useful response type
+ } catch (IOException e) {
+ LOG.info("Log error: " + e);
+ }
+ }
+
+ /**
+ * Performs a blocking search to Cluestick service.
+ *
+ * @param symbol The symbol to search for.
+ * @return The results found on Cluestick.
+ * @throws IOException on a network error
+ */
+ public List<Result> performSearch(Symbol symbol, @Nullable String userAgent) throws IOException {
+ Search.SearchOperation request = searchService.search();
+ request.setSessionId(sessionId);
+ request.setSymbol(symbol.symbolName);
+ request.setPackage(symbol.packageName);
+ // Assume lang is always java for now
+ request.setLang("java");
+ // TODO(thoroogod): For now, request all contents of result files. This may not be actually
+ // that practical in terms of bandwidth.
+ request.setAllContext(true);
+ if (userAgent == null) {
+ request.setUserAgent(userAgent);
+ } else {
+ request.setUserAgent(DEFAULT_USER_AGENT);
+ }
+
+ SearchResponse response = request.execute();
+ if (response.getSessionId() != null) {
+ // Save any server-hinted session ID.
+ sessionId = response.getSessionId();
+ LOG.info("Using updated sessionId: " + sessionId);
+ }
+ return response.getResults();
+ }
+
+}
diff --git a/src/com/google/devrel/cluestick/searchservice/EventLog.java b/src/com/google/devrel/cluestick/searchservice/EventLog.java
new file mode 100644
index 0000000..97b60e9
--- /dev/null
+++ b/src/com/google/devrel/cluestick/searchservice/EventLog.java
@@ -0,0 +1,124 @@
+/*
+ * 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.devrel.cluestick.searchservice;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Logging helper for Cluestick.
+ */
+public class EventLog {
+ private static final long LOG_DELAY_SEC = 30;
+
+ private final CluestickSearch cluestick;
+ private final Map<PendingEvent, Integer> pending;
+ private final Timer timer;
+
+ public EventLog(CluestickSearch cluestick) {
+ this.cluestick = cluestick;
+ timer = new Timer(true);
+ pending = new HashMap<PendingEvent, Integer>();
+ }
+
+ /**
+ * Releases resources used by this {@link EventLog}.
+ */
+ public void cancel() {
+ timer.cancel();
+ }
+
+ /**
+ * Asynchronously logs an event to the Cluestick service.
+ *
+ * @param action The string action, e.g. "copy".
+ * @param resultKey The result being operated on, if any.
+ * @param signal Either a positive or negative signal, e.g. for code result quality.
+ */
+ public void logEvent(@Nullable String action, @Nullable String resultKey, int signal) {
+ boolean scheduleTask;
+ synchronized (pending) {
+ scheduleTask = pending.isEmpty();
+
+ PendingEvent eventKey = new PendingEvent(action, resultKey);
+ Integer signalObject = pending.get(eventKey);
+ if (signalObject == null) {
+ signalObject = new Integer(signal);
+ } else {
+ signalObject = new Integer(signalObject.intValue() + signal);
+ }
+ pending.put(eventKey, signalObject);
+ }
+
+ if (scheduleTask) {
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ submitEvents();
+ }
+ }, TimeUnit.SECONDS.toMillis(LOG_DELAY_SEC));
+ }
+ }
+
+ /**
+ * Submits all pending events to the Cluestick service.
+ */
+ private void submitEvents() {
+ Map<PendingEvent, Integer> active;
+ synchronized (pending) {
+ active = new HashMap<PendingEvent, Integer>(pending);
+ pending.clear();
+ }
+ for (Map.Entry<PendingEvent, Integer> entry : active.entrySet()) {
+ PendingEvent eventKey = entry.getKey();
+ cluestick.logEvent(eventKey.action, eventKey.resultKey, entry.getValue().intValue());
+ }
+ }
+
+ private static class PendingEvent {
+ final String action;
+ final String resultKey;
+
+ PendingEvent(String action, String resultKey) {
+ this.action = action;
+ this.resultKey = resultKey;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PendingEvent)) {
+ return false;
+ }
+ PendingEvent other = (PendingEvent) o;
+ return Objects.equals(action, other.action) && Objects.equals(resultKey, other.resultKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(action, resultKey);
+ }
+ }
+}
+
diff --git a/src/com/google/devrel/cluestick/studioclient/Browser.java b/src/com/google/devrel/cluestick/studioclient/Browser.java
new file mode 100644
index 0000000..87be257
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/Browser.java
@@ -0,0 +1,47 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.appspot.cluestick_server.search.model.Result;
+
+import org.jetbrains.annotations.NotNull;
+
+import javax.swing.JPanel;
+
+/**
+ * Browser describes a browser to show a SearchResult inside a built instance
+ * of JPanel.
+ */
+public interface Browser {
+
+ /**
+ * Clears a shown result.
+ */
+ void showEmpty();
+
+ /**
+ * Shows the given result in this Browser.
+ *
+ * @param result to show
+ */
+ void showResult(@NotNull Result result);
+
+ /**
+ * Returns the JPanel to show that contains the empty/shown result content.
+ */
+ JPanel getPanel();
+
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/CodeBrowser.java b/src/com/google/devrel/cluestick/studioclient/CodeBrowser.java
new file mode 100644
index 0000000..703efc2
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/CodeBrowser.java
@@ -0,0 +1,305 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.google.devrel.cluestick.searchservice.EventLog;
+
+import com.appspot.cluestick_server.search.model.CodeResult;
+import com.appspot.cluestick_server.search.model.Result;
+import com.appspot.cluestick_server.search.model.ResultContext;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.editor.Document;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.EditorFactory;
+import com.intellij.openapi.editor.EditorSettings;
+import com.intellij.openapi.editor.LogicalPosition;
+import com.intellij.openapi.editor.ScrollType;
+import com.intellij.openapi.editor.TextAnnotationGutterProvider;
+import com.intellij.openapi.editor.colors.EditorFontType;
+import com.intellij.openapi.editor.event.SelectionEvent;
+import com.intellij.openapi.editor.event.SelectionListener;
+import com.intellij.openapi.editor.ex.EditorEx;
+import com.intellij.openapi.editor.ex.EditorGutterComponentEx;
+import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory;
+import com.intellij.openapi.editor.markup.EffectType;
+import com.intellij.openapi.editor.markup.HighlighterLayer;
+import com.intellij.openapi.editor.markup.MarkupModel;
+import com.intellij.openapi.editor.markup.TextAttributes;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.util.TextRange;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.util.ui.UIUtil;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.BorderLayout;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+
+/**
+ * CodeBrowser implements Browser to show Cluestick search results containing code.
+ */
+class CodeBrowser extends JPanel implements Browser, Disposable {
+ private static final Color HIGHLIGHT_BACKGROUND = UIUtil.getTreeUnfocusedSelectionBackground();
+ private static final TextAttributes HIGHLIGHT_ATTRIBUTES =
+ new TextAttributes(null, HIGHLIGHT_BACKGROUND, null, EffectType.SEARCH_MATCH, Font.BOLD);
+ private static final Pattern WHITESPACE_PREFIX = Pattern.compile("^\\s*");
+
+ private final EventLog eventLog;
+ private final EditorEx editor;
+ private final Document document;
+ private final Project project;
+ private final EditorHighlighterFactory highlighterFactory;
+ private Result result;
+
+ public CodeBrowser(@NotNull Project project, @NotNull EventLog eventLog) {
+ super(new BorderLayout());
+ this.project = project;
+ this.eventLog = eventLog;
+
+ highlighterFactory = EditorHighlighterFactory.getInstance();
+
+ EditorFactory factory = EditorFactory.getInstance();
+ document = factory.createDocument("");
+ editor = (EditorEx) factory.createViewer(document, project);
+
+ EditorSettings settings = editor.getSettings();
+ settings.setLineNumbersShown(true);
+ settings.setAnimatedScrolling(false);
+ settings.setRefrainFromScrolling(true);
+
+ EditorGutterComponentEx gutter = editor.getGutterComponentEx();
+ gutter.setShowDefaultGutterPopup(false);
+ gutter.revalidateMarkup();
+
+ editor.getSelectionModel().addSelectionListener(new SelectionListener() {
+ @Override
+ public void selectionChanged(SelectionEvent e) {
+ TextRange range = e.getNewRange();
+ if (range.isEmpty()) {
+ return;
+ }
+ // TODO(thorogood): event log constants
+ CodeBrowser.this.eventLog.logEvent("select", getResultKey(), 0);
+ }
+ });
+
+ // Add the component. Configure the preferred size to zero, as otherwise the Editor itself will
+ // grow rather than fitting inside its scroll pane.
+ JComponent component = editor.getComponent();
+ component.setPreferredSize(new Dimension(0, 0));
+ add(component, BorderLayout.CENTER);
+ }
+
+ @Override
+ public void dispose() {
+ EditorFactory factory = EditorFactory.getInstance();
+ factory.releaseEditor(editor);
+ }
+
+ @Override
+ public void showEmpty() {
+ result = null;
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.setText("");
+ }
+ });
+ }
+
+ @Override
+ public void showResult(Result result) {
+ if (result == null || result.getCode() == null) {
+ showEmpty();
+ return;
+ }
+ this.result = result;
+
+ CodeResult code = result.getCode();
+ List<CharSequence> lines = new ArrayList<CharSequence>();
+ int scrollTo = -1;
+
+ List<Integer> highlightLines = new ArrayList<Integer>();
+ List<ResultContext> allContext = code.getContext();
+ if (allContext != null && !allContext.isEmpty()) {
+ // If there's only a single result and it's at line one, it's likely the whole file.
+ // Otherwise, the results contain snippets that can be displayed separately.
+ boolean containsSnippets = !(allContext.size() == 1 && allContext.get(0).getStartAt() == 1);
+
+ for (ResultContext context : allContext) {
+ List<String> src = context.getSrc();
+ if (src == null || src.isEmpty()) {
+ continue;
+ }
+
+ src = formatCode(src);
+ if (!containsSnippets) {
+ if (scrollTo == -1 && !context.getResultsAt().isEmpty()) {
+ scrollTo = context.getResultsAt().get(0);
+ }
+ for (int line : context.getResultsAt()) {
+ highlightLines.add(line - 1);
+ }
+ lines.addAll(formatCode(src));
+ continue;
+ }
+
+ // If this is a snippet, then annotate it as such.
+ // TODO(thorogood): Different annotations if we ever show other languages (probably just
+ // Python).
+ StringBuilder dividerBuilder = new StringBuilder();
+ int startAt = context.getStartAt();
+ int endAt = startAt + src.size() - 1;
+ if (endAt == startAt) {
+ dividerBuilder.append("// Line ").append(startAt);
+ } else {
+ dividerBuilder.append(String.format("// Lines %d-%d", startAt, endAt));
+ String resultsAt = StringUtil.join(context.getResultsAt(), ", ");
+ if (!StringUtil.isEmpty(resultsAt)) {
+ dividerBuilder.append(" (").append(resultsAt).append(')');
+ }
+ }
+ lines.add(dividerBuilder);
+ for (int line : context.getResultsAt()) {
+ highlightLines.add(lines.size() + line - context.getStartAt());
+ }
+ lines.addAll(formatCode(src));
+ lines.add("");
+ }
+ }
+
+ if (scrollTo < 1) {
+ scrollTo = 1;
+ }
+ // LogicalPosition is zero-indexed, but our code is one-indexed.
+ LogicalPosition position = new LogicalPosition(scrollTo - 1, 0);
+ setText(lines, position, highlightLines);
+
+ String path = ResultUtils.getBaseName(code.getPath());
+ editor.setHighlighter(highlighterFactory.createEditorHighlighter(project, path));
+ }
+
+ @Override
+ public JPanel getPanel() {
+ return this;
+ }
+
+ /**
+ * @return The result key for the result currently being shown, if available.
+ */
+ @Nullable
+ private String getResultKey() {
+ if (result != null) {
+ return result.getKey();
+ }
+ return null;
+ }
+
+ /**
+ * Sets the text and cursor position in the current {@link EditorEx}.
+ * @param lines The lines of code to render.
+ * @param position The position to center on once lines are set.
+ */
+ private void setText(@NotNull List<? extends CharSequence> lines,
+ @NotNull final LogicalPosition position,
+ @NotNull final List<Integer> highlight) {
+ final MarkupModel markup = editor.getMarkupModel();
+ markup.removeAllHighlighters();
+
+ final StringBuilder text = new StringBuilder();
+ for (CharSequence line : lines) {
+ text.append(line).append('\n');
+ }
+ ApplicationManager.getApplication().runWriteAction(new Runnable() {
+ @Override
+ public void run() {
+ document.setText(text);
+ editor.getCaretModel().moveToLogicalPosition(position);
+ editor.getScrollingModel().scrollToCaret(ScrollType.CENTER);
+
+ int layer = HighlighterLayer.SELECTION - 1;
+ for (int line : highlight) {
+ markup.addLineHighlighter(line, layer, HIGHLIGHT_ATTRIBUTES);
+ }
+
+ editor.getGutterComponentEx().revalidateMarkup();
+ }
+ });
+ }
+
+ /**
+ * Formats code for display. Currently just trims left whitespace.
+ *
+ * @param input Source code to format.
+ * @return Formatted source code.
+ */
+ @NotNull
+ public static List<String> formatCode(@NotNull List<String> input) {
+ boolean knownPrefix = false;
+ String prefix = "";
+
+ for (String line : input) {
+ Matcher m = WHITESPACE_PREFIX.matcher(line);
+ if (!m.find()) {
+ throw new IllegalStateException("empty regex should always match");
+ }
+ if (m.hitEnd()) {
+ continue; // blank line
+ }
+ String localPrefix = line.substring(0, m.end());
+ if (!knownPrefix) {
+ prefix = localPrefix;
+ knownPrefix = true;
+ continue;
+ }
+
+ // Find minimum common substring of prefix/localPrefix.
+ int j = 0;
+ int len = Math.min(prefix.length(), localPrefix.length());
+ while (j < len && prefix.charAt(j) == localPrefix.charAt(j)) {
+ ++j;
+ }
+ prefix = prefix.substring(0, j);
+ }
+
+ if (prefix.isEmpty()) {
+ return input;
+ }
+ int plen = prefix.length();
+ List<String> output = new ArrayList<String>(input.size());
+ for (String src : input) {
+ if (src.length() < plen) {
+ output.add("");
+ } else {
+ output.add(src.substring(plen));
+ }
+ }
+ return output;
+ }
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/DynamicToolWindowWrapper.java b/src/com/google/devrel/cluestick/studioclient/DynamicToolWindowWrapper.java
new file mode 100644
index 0000000..23a3b83
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/DynamicToolWindowWrapper.java
@@ -0,0 +1,87 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.appspot.cluestick_server.search.model.Result;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.openapi.wm.ToolWindowAnchor;
+import com.intellij.openapi.wm.ToolWindowManager;
+import com.intellij.ui.content.Content;
+import com.intellij.ui.content.ContentFactory;
+import com.intellij.ui.content.ContentManager;
+
+import java.util.List;
+
+/**
+ * Wrapper of {@link ToolWindow}, which contains a instance of {@link ToolWindow} and it can be
+ * registered dynamically.
+ * To show a ToolWindow from {@link AnAction#actionPerformed}, call like following:
+ * <pre>
+ * @Override
+ * public void actionPerformed(AnActionEvent event) {
+ * Project project = getEventProject(event);
+ * DynamicToolWindowWrapper toolWindowWrapper = DynamicToolWindowWrapper.getInstance(project);
+ * ToolWindow toolWindow = toolWindowWrapper.getToolWindow();
+ * toolWindow.show((Runnable) null);
+ * .....
+ * }
+ * </pre>
+ */
+public class DynamicToolWindowWrapper {
+ private static final String TOOL_WINDOW_TAG = "Find Sample Code";
+ private static final String TOOL_WINDOW_TITLE = "for %s";
+ private Project project;
+ private ToolWindow toolWindow;
+
+ public DynamicToolWindowWrapper(Project project) {
+ this.project = project;
+ }
+
+ public static DynamicToolWindowWrapper getInstance(Project project) {
+ return ServiceManager.getService(project, DynamicToolWindowWrapper.class);
+ }
+
+ public ToolWindow getToolWindow(Symbol symbol, List<Result> results) {
+ if (toolWindow == null) {
+ toolWindow = ToolWindowManager.getInstance(project).registerToolWindow(TOOL_WINDOW_TAG, true,
+ ToolWindowAnchor.BOTTOM);
+ toolWindow.setIcon(IconFetcher.GoogleDevelopers);
+ }
+ toolWindow.show(null);
+
+ SearchResultsView view = new SearchResultsView(project, symbol, results);
+ view.setClose(new Runnable() {
+ @Override
+ public void run() {
+ toolWindow.hide(null);
+ }
+ });
+
+ String title = String.format(TOOL_WINDOW_TITLE, symbol.symbolName);
+ Content content = ContentFactory.SERVICE.getInstance().createContent(view, title, false);
+ content.setDisposer(view);
+ content.setShouldDisposeContent(false); // prevent multiple dispose
+
+ ContentManager contentManager = toolWindow.getContentManager();
+ contentManager.removeAllContents(true);
+ contentManager.addContent(content);
+ contentManager.setSelectedContent(content);
+ return toolWindow;
+ }
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/FindSampleUsageAction.java b/src/com/google/devrel/cluestick/studioclient/FindSampleUsageAction.java
new file mode 100644
index 0000000..244abd8
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/FindSampleUsageAction.java
@@ -0,0 +1,189 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.google.devrel.cluestick.searchservice.CluestickSearch;
+
+import com.appspot.cluestick_server.search.model.Result;
+import com.intellij.codeInsight.CodeInsightActionHandler;
+import com.intellij.codeInsight.TargetElementUtilBase;
+import com.intellij.codeInsight.actions.CodeInsightAction;
+import com.intellij.codeInsight.hint.HintManager;
+import com.intellij.find.actions.FindUsagesInFileAction;
+import com.intellij.ide.plugins.IdeaPluginDescriptor;
+import com.intellij.ide.plugins.PluginManager;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.extensions.PluginId;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.wm.ToolWindow;
+import com.intellij.psi.PsiDocumentManager;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiJavaCodeReferenceElement;
+import com.intellij.psi.PsiReference;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Action to provide searching of the currently selected text in samples.
+ */
+public class FindSampleUsageAction extends CodeInsightAction implements CodeInsightActionHandler {
+
+ // TODO(thorogood): Don't inline strings in this class. Viva la i18n!
+
+ private static final Logger LOG = Logger
+ .getInstance("#com.google.devrel.cluestick.studioclient.FindSampleUsageAction");
+ public static final String PLUGIN_ID = "com.google.cluestick.studioclient";
+
+ @Override
+ public void update(AnActionEvent event) {
+ FindUsagesInFileAction.updateFindUsagesAction(event);
+ }
+
+ @Override
+ public void invoke(@NotNull final Project project, @NotNull final Editor editor,
+ @NotNull PsiFile psiFile) {
+ PsiDocumentManager.getInstance(project).commitAllDocuments();
+
+ int offset = editor.getCaretModel().getOffset();
+ final Symbol symbol = findSymbol(editor, offset);
+ if (symbol == null) {
+ showMessage(editor, "Please highlight a variable, type or method");
+ return;
+ }
+
+ IdeaPluginDescriptor pluginDescriptor = PluginManager.getPlugin(PluginId.getId(PLUGIN_ID));
+ final String userAgent = pluginDescriptor.getName() + "/" + pluginDescriptor.getVersion();
+
+ ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
+ @Override
+ public void run() {
+ List<Result> results;
+ try {
+ results = ServiceManager.getService(CluestickSearch.class).performSearch(
+ symbol, userAgent);
+ } catch (IOException ex) {
+ LOG.warn("Couldn't perform Cluestick search", ex);
+ showMessage(editor, String.format("Samples are currently unavailable for: %s", symbol));
+ return;
+ }
+ if (results.isEmpty()) {
+ showMessage(editor, String.format("No samples found for: %s", symbol));
+ } else {
+ showSamplesToolWindow(project, symbol, results);
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean startInWriteAction() {
+ return false;
+ }
+
+ @NotNull
+ @Override
+ protected CodeInsightActionHandler getHandler() {
+ return this;
+ }
+
+ /**
+ * Finds the symbol name under the given position. This is similar to the behavior of the
+ * standard "Find Usages" action, except that-
+ * - this returns a binary/internal symbol, rather than the ref in user code
+ * - it falls back to returning an unmatched string
+ *
+ * @param editor To look within
+ * @param offset To search at
+ * @return The name of a matched symbol, or null
+ */
+ @Nullable
+ private Symbol findSymbol(@NotNull Editor editor, int offset) {
+ TargetElementUtilBase util = TargetElementUtilBase.getInstance();
+ PsiElement targetElement = util.findTargetElement(editor, util.getAllAccepted(), offset);
+
+ Collection<Symbol> out = PsiHelpers.findSymbols(targetElement);
+ if (out.isEmpty()) {
+ // This could be an unindexed element - aka appearing in red. Fallback to a traversal,
+ // just to try to get a basic symbol out.
+ PsiReference reference = TargetElementUtilBase.findReference(editor, offset);
+ if (reference instanceof PsiJavaCodeReferenceElement) {
+ String qualifiedName = ((PsiJavaCodeReferenceElement) reference).getQualifiedName();
+ if (qualifiedName != null) {
+ return new Symbol(qualifiedName);
+ }
+ }
+ return null;
+ }
+ LOG.info("Symbols under cursor: " + out);
+
+ // Find the first symbol with a package name, or find the first fallback without one.
+ // TODO(thorogood): Order these at some point (unqualified last) and search for all.
+ Symbol fallback = null;
+ for (Symbol symbol : out) {
+ if (symbol.isQualified()) {
+ return symbol;
+ }
+ if (fallback == null) {
+ fallback = symbol;
+ }
+ }
+ return fallback;
+ }
+
+ /**
+ * Shows the list of results in a toolwindow panel.
+ *
+ * @param project The project.
+ * @param symbol The symbol selected in IntelliJ.
+ * @param results List of SearchResult objects from cloud endpoint generated lib.
+ */
+ private void showSamplesToolWindow(@NotNull final Project project, final Symbol symbol,
+ final List<Result> results) {
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ DynamicToolWindowWrapper toolWindowWrapper = DynamicToolWindowWrapper.getInstance(project);
+ ToolWindow toolWindow = toolWindowWrapper.getToolWindow(symbol, results);
+ toolWindow.show((Runnable) null);
+ }
+ });
+ }
+
+ /**
+ * Shows an error message.
+ *
+ * @param editor The editor to show a message on.
+ * @param message The error message.
+ */
+ private void showMessage(final Editor editor, final String message) {
+ ApplicationManager.getApplication().invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ HintManager.getInstance().showErrorHint(editor, message);
+ }
+ });
+ }
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/IconFetcher.java b/src/com/google/devrel/cluestick/studioclient/IconFetcher.java
new file mode 100644
index 0000000..a07f23c
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/IconFetcher.java
@@ -0,0 +1,34 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.intellij.openapi.util.IconLoader;
+
+import javax.swing.Icon;
+
+/**
+ * A class responsible for fetching various icons.
+ */
+public class IconFetcher {
+ private static Icon load(String path) {
+ return IconLoader.getIcon(path, IconFetcher.class);
+ }
+
+ public static final Icon GoogleDevelopers = null;
+// load("/com/google/devrel/cluestick/studioclient/developers.png");
+ public static final Icon GitHub = null;
+// load("/com/google/devrel/cluestick/studioclient/github.png");
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/PsiHelpers.java b/src/com/google/devrel/cluestick/studioclient/PsiHelpers.java
new file mode 100644
index 0000000..a3725c9
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/PsiHelpers.java
@@ -0,0 +1,294 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.intellij.psi.PsiArrayType;
+import com.intellij.psi.PsiClass;
+import com.intellij.psi.PsiClassType;
+import com.intellij.psi.PsiElement;
+import com.intellij.psi.PsiFile;
+import com.intellij.psi.PsiInvalidElementAccessException;
+import com.intellij.psi.PsiMember;
+import com.intellij.psi.PsiMethod;
+import com.intellij.psi.PsiModifier;
+import com.intellij.psi.PsiType;
+import com.intellij.psi.PsiTypeElement;
+import com.intellij.psi.PsiVariable;
+import com.intellij.psi.util.MethodSignatureBackedByPsiMethod;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility methods for working with Psi* objects from IntelliJ.
+ */
+class PsiHelpers {
+
+ private PsiHelpers() {
+ }
+
+ /**
+ * Finds symbol names from internal classes, based on the given target element.
+ *
+ * The most relevant symbols are returned earlier in the {@link Collection}.
+ *
+ * @param targetElement To examine for symbols.
+ * @return Names of identified symbols.
+ */
+ @NotNull
+ public static Collection<Symbol> findSymbols(@Nullable PsiElement targetElement) {
+ if (targetElement == null) {
+ return Collections.emptyList();
+ }
+ Helper helper = new Helper(targetElement);
+ return helper.symbols;
+ }
+
+ /**
+ * Finds the nearest {@link PsiClass}, by superclass, that is in an internal/binary class file.
+ *
+ * @param c The class to search from.
+ * @return The nearest binary class, ignoring {@code Object}, possibly {@code null}.
+ */
+ @Nullable
+ public static PsiClass nearestBinaryClass(@Nullable PsiClass c) {
+ while (c != null) {
+ if (classIsObject(c)) {
+ break;
+ } else if (elementInBinaryClass(c)) {
+ return c;
+ }
+ c = c.getSuperClass();
+ }
+ return null;
+ }
+
+ /**
+ * Returns whether the passed class is {@code Object}.
+ *
+ * @param c The class to check,
+ * @return Whether this is "java.lang.Object".
+ */
+ public static boolean classIsObject(@Nullable PsiClass c) {
+ // TODO: actually compare to java.lang.Object?
+ return c.getSuperClass() == null;
+ }
+
+ /**
+ * Returns whether the target element is internal, aka in a binary class file. Uses various
+ * checks to confirm this, including file type, file not writable, or just prefix matching.
+ *
+ * @param targetElement The target element to check.
+ * @return Whether this target element is located in a binary class file.
+ */
+ public static boolean elementInBinaryClass(@Nullable PsiElement targetElement) {
+ if (targetElement == null) {
+ return false;
+ }
+ targetElement = targetElement.getOriginalElement();
+ PsiFile file = null;
+ try {
+ file = targetElement.getContainingFile();
+ } catch (PsiInvalidElementAccessException e) {
+ // fine, ignore
+ }
+
+ // If the element comes from a binary file, assume it's indexable.
+ if (file != null && file.getFileType().isBinary()) {
+ return true;
+ }
+
+ // Assume that unwriteable code is internal (e.g., loaded Android source files).
+ if (!targetElement.isWritable()) {
+ return true;
+ }
+
+ // Use a lazy fallback if all else fails - check for "java." or "android." prefixes.
+ PsiClass relatedClass = classForElement(targetElement);
+ if (relatedClass != null) {
+ String name = relatedClass.getQualifiedName();
+ if (name != null) {
+ boolean isInternal = name.startsWith("java.") || name.startsWith("android.");
+ if (isInternal) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the closely related {@link PsiClass} for this {@link PsiElement}, if possible.
+ *
+ * This returns itself or the owning class.
+ *
+ * @param element The element to examine.
+ * @return The closely related {@link PsiClass}.
+ */
+ @Nullable
+ public static PsiClass classForElement(@Nullable PsiElement element) {
+ if (element instanceof PsiClass) {
+ return (PsiClass) element;
+ }
+ if (element instanceof PsiMember) {
+ return ((PsiMember) element).getContainingClass();
+ }
+ if (element instanceof PsiVariable) {
+ // falls through to next if block
+ element = ((PsiVariable) element).getTypeElement();
+ }
+ if (element instanceof PsiTypeElement) {
+ PsiType type = ((PsiTypeElement) element).getType();
+ if (type instanceof PsiArrayType) {
+ type = ((PsiArrayType) type).getComponentType();
+ }
+ if (type instanceof PsiClassType) {
+ return ((PsiClassType) type).resolve();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper is a private static helper class to generate symbol names from a {@link PsiElement}.
+ *
+ * Yields symbols from the constructor, so this class is single-use.
+ */
+ private static class Helper {
+ final List<Symbol> symbols = new ArrayList<Symbol>();
+
+ Helper(@NotNull PsiElement targetElement) {
+
+ // PsiMember is a member, e.g., a field (enum/variable) or method, on a class.
+ if (targetElement instanceof PsiMember) {
+ yield((PsiMember) targetElement);
+ }
+
+ // PsiMethod is a method on a class. Find all the methods it overrides, regardless of how
+ // far away they were defined (e.g., will match onCreate from numerous superclasses).
+ if (targetElement instanceof PsiMethod) {
+ // TODO(thorogood): Is this returned in a sane order (closest first)?
+ List<PsiMethod> queue = new ArrayList<PsiMethod>();
+ queue.add((PsiMethod) targetElement);
+
+ for (int i = 0; i < queue.size(); ++i) {
+ PsiMethod method = queue.get(i);
+ if (i > 0 && !yield(method)) { // don't yield first method, done as PsiMember above
+ continue;
+ }
+ List<MethodSignatureBackedByPsiMethod> supers =
+ method.findSuperMethodSignaturesIncludingStatic(false);
+ for (MethodSignatureBackedByPsiMethod sig : supers) {
+ queue.add(sig.getMethod());
+ }
+ }
+ }
+
+ // PsiVariable is a variable (including a PsiField, which is yielded above). Examine its type.
+ if (targetElement instanceof PsiVariable) {
+ targetElement = ((PsiVariable) targetElement).getTypeElement();
+ }
+
+ // PsiTypeElement is a literal use of e.g., GoogleMap in code, as a return type or other type.
+ // It's not a class definition itself - that's PsiClass, below.
+ if (targetElement instanceof PsiTypeElement) {
+ PsiType type = ((PsiTypeElement) targetElement).getType();
+ if (type instanceof PsiArrayType) {
+ type = ((PsiArrayType) type).getComponentType();
+ }
+ if (type instanceof PsiClassType) {
+ targetElement = ((PsiClassType) type).resolve();
+ } else {
+ yield(type.getCanonicalText()); // probably int, float etc
+ }
+ }
+
+ // PsiClass is the definition of a class, e.g. "public class Foo {", or an anonymous class.
+ // This is the last block, so try to aggressively match the class related to this element.
+ PsiClass targetClass = null;
+ if (targetElement instanceof PsiClass) {
+ targetClass = (PsiClass) targetElement;
+ } else if (symbols.isEmpty()) {
+ // no other symbols found, try to find something
+ targetClass = classForElement(targetElement);
+ }
+ if (targetClass != null) {
+ PsiClass current = targetClass;
+
+ // Yield all binary super classes, in order closest -> farthest. This will yield e.g.,
+ // MainActivity to [Activity,Context].
+ while ((current = nearestBinaryClass(current)) != null) {
+ yield(current);
+ current = current.getSuperClass();
+ }
+
+ // Yield only directly implemented interfaces.
+ PsiClass interfaces[] = targetClass.getInterfaces();
+ for (PsiClass i : interfaces) {
+ yield(i);
+ }
+ }
+ }
+
+ /**
+ * @param s String symbol to yield, if non-empty.
+ * @return Whether the symbol was yielded.
+ */
+ private boolean yield(@Nullable String s) {
+ if (s != null && !s.isEmpty()) {
+ symbols.add(new Symbol(s));
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @param c Class to yield, if non-null and with a qualified name.
+ * @return Whether the class was yielded.
+ */
+ private boolean yield(@Nullable PsiClass c) {
+ if (elementInBinaryClass(c)) {
+ return yield(c.getQualifiedName());
+ }
+ return false;
+ }
+
+ /**
+ * @param m Member to yield, including an unqualified name.
+ * @return Whether the member was yielded.
+ */
+ private boolean yield(@Nullable PsiMember m) {
+ if (m.hasModifierProperty(PsiModifier.PRIVATE)) {
+ return false;
+ }
+ PsiClass containingClass = m.getContainingClass();
+ String classPrefix = "";
+ if (elementInBinaryClass(containingClass)) {
+ String qualifiedName = containingClass.getQualifiedName();
+ if (qualifiedName != null) {
+ classPrefix = String.format("%s.", qualifiedName);
+ }
+ }
+ return yield(String.format("%s%s", classPrefix, m.getName()));
+ }
+ }
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/ResultUtils.java b/src/com/google/devrel/cluestick/studioclient/ResultUtils.java
new file mode 100644
index 0000000..538003c
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/ResultUtils.java
@@ -0,0 +1,75 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Result utility library for the Cluestick IntelliJ plugin. This is intentionally package-private.
+ */
+class ResultUtils {
+
+ /**
+ * Value type for a GitHub repo.
+ */
+ public static class GitHubInfo {
+ public final String user;
+ public final String repo;
+
+ public GitHubInfo(String user, String repo) {
+ this.user = user;
+ this.repo = repo;
+ }
+ }
+
+ private ResultUtils() {}
+
+ /**
+ * Gets the basename of this path (e.g. foo/bar/bing.foo => bing.foo).
+ * @param path The entire input path.
+ * @return The basename of this path.
+ */
+ public static String getBaseName(@Nullable String path) {
+ if (path != null) {
+ int index = path.lastIndexOf('/');
+ if (index >= 0) {
+ return path.substring(index + 1);
+ }
+ }
+ return path;
+ }
+
+ /**
+ * Builds a GitHubInfo pair from a raw source name, if possible.
+ * @param repo The raw source name.
+ * @return The possibly parsed GitHubInfo.
+ */
+ public static GitHubInfo parseRepoName(@Nullable String repo) {
+ if (repo == null) {
+ return null;
+ }
+ String[] repoBits = repo.replace("github.com/", "").split("/");
+ if (repoBits.length >= 2) {
+ return new GitHubInfo(repoBits[0], repoBits[1]);
+ }
+ return null;
+ }
+
+ public static String escapeHTML(String raw) {
+ return raw.replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;");
+ }
+
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/SearchResultsNode.java b/src/com/google/devrel/cluestick/studioclient/SearchResultsNode.java
new file mode 100644
index 0000000..9c8e3e4
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/SearchResultsNode.java
@@ -0,0 +1,39 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.appspot.cluestick_server.search.model.Result;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * SearchResultsNode is used to describe a node inside {@link SearchResultsView}.
+ */
+interface SearchResultsNode {
+
+ /**
+ * Returns the URL that should be opened if this node is actioned.
+ */
+ @Nullable
+ String getURL();
+
+ /**
+ * Returns the search result that this node represents.
+ */
+ @Nullable
+ Result getSearchResult();
+
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/SearchResultsTree.java b/src/com/google/devrel/cluestick/studioclient/SearchResultsTree.java
new file mode 100644
index 0000000..c8f0c67
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/SearchResultsTree.java
@@ -0,0 +1,289 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.appspot.cluestick_server.search.model.CodeResult;
+import com.appspot.cluestick_server.search.model.Result;
+
+import com.intellij.icons.AllIcons;
+import com.intellij.ui.ColoredTreeCellRenderer;
+import com.intellij.ui.SimpleTextAttributes;
+import com.intellij.ui.treeStructure.Tree;
+import com.intellij.util.ui.UIUtil;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.Icon;
+import javax.swing.JTree;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+
+/**
+ * Tree structure containing search results for display in a JTree. Results are displayed as a
+ * collection of repos. This tree is not expanded by default.
+ */
+public final class SearchResultsTree extends Tree {
+ private static final SimpleTextAttributes SUFFIX_ATTRIBUTES = new SimpleTextAttributes(
+ SimpleTextAttributes.STYLE_ITALIC, UIUtil.getInactiveTextColor());
+
+ private final LayoutNode symbolHeading;
+ private final LayoutNode resultHeading;
+
+ public SearchResultsTree(Symbol symbol, List<Result> results) {
+ super(new DefaultMutableTreeNode());
+ DefaultMutableTreeNode top = (DefaultMutableTreeNode) getModel().getRoot();
+
+ setRootVisible(false);
+ setCellRenderer(new CellRenderer());
+
+ symbolHeading = new LayoutNode();
+ symbolHeading.text = "Symbol";
+ symbolHeading.isHeading = true;
+ top.add(symbolHeading);
+
+ LayoutNode symbolNode = new LayoutNode();
+ if (symbol.packageName != null && !symbol.packageName.isEmpty()) {
+ symbolNode.prefix = symbol.packageName + ".";
+ }
+ symbolNode.icon = AllIcons.Nodes.EjbFinderMethod;
+ symbolNode.text = symbol.symbolName;
+ symbolHeading.add(symbolNode);
+
+ resultHeading = new LayoutNode();
+ resultHeading.text = "Found results";
+ resultHeading.count = results.size();
+ resultHeading.isHeading = true;
+ top.add(resultHeading);
+
+ addResults(resultHeading, results);
+ }
+
+ /**
+ * Determines whether the {@link Result} has code.
+ * @param result The result to check.
+ * @return Whether the result safely has code.
+ */
+ public static boolean hasCode(Result result) {
+ return result.getCode() != null && result.getCode().getLines() != null;
+ }
+
+ private void addResults(DefaultMutableTreeNode parent, List<Result> results) {
+ Map<String, RepoNode> repoNodes = new LinkedHashMap<String, RepoNode>(); // linked provides insert ordering
+
+ for (Result result : results) {
+ if (!hasCode(result)) {
+ parent.add(new ResultNode(result));
+ continue;
+ }
+
+ // If this is a code node, then group by repo/branch pair. Retrieve or create the RepoNode
+ // for this pair, add and update its total count.
+ CodeResult code = result.getCode();
+ String key = String.format("%s:%s", code.getRepo(), code.getBranch());
+ RepoNode repoNode = repoNodes.get(key);
+ if (repoNode == null) {
+ repoNode = new RepoNode(code.getRepo(), code.getBranch());
+ repoNodes.put(key, repoNode);
+ }
+ repoNode.add(new ResultNode(result));
+ }
+
+ // Now, add the repo nodes in order of first seen.
+ for (RepoNode repoNode : repoNodes.values()) {
+ parent.add(repoNode);
+ }
+ }
+
+ /**
+ * RepoNode represents a whole repository (aka, repo/branch).
+ */
+ private static class RepoNode extends DefaultMutableTreeNode implements SearchResultsNode {
+ private final String repo;
+ private final String branch;
+ private final ResultUtils.GitHubInfo info;
+
+ RepoNode(String repo, String branch) {
+ this.repo = repo;
+ this.branch = branch;
+
+ info = ResultUtils.parseRepoName(repo);
+ }
+
+ public Icon getIcon() {
+ if (info != null) {
+ return IconFetcher.GitHub;
+ }
+ return null;
+ }
+
+ public String getSuffix() {
+ if (this.branch == null || this.branch.isEmpty() || this.branch.equals("master")) {
+ return null;
+ }
+ return this.branch;
+ }
+
+ public String getPrefix() {
+ if (info != null) {
+ return info.user + "/";
+ }
+ return null;
+ }
+
+ @Override
+ public String getURL() {
+ if (info != null) {
+ // TODO(thorogood): Deal with non-master branch.
+ return String.format("https://github.com/%s/%s", info.user, info.repo);
+ }
+ return null;
+ }
+
+ @Override
+ public Result getSearchResult() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ if (info != null) {
+ return info.repo;
+ }
+ return this.repo;
+ }
+ }
+
+
+ /**
+ * LayoutNode is a generic node for use inside SearchResultsTree.
+ */
+ private static class LayoutNode extends DefaultMutableTreeNode {
+ String prefix;
+ String suffix;
+ String text;
+ Icon icon;
+ boolean isHeading;
+ int count = -1;
+
+ @NotNull
+ public String getText() {
+ if (text == null) {
+ return "?";
+ }
+ return text;
+ }
+ }
+
+ /**
+ * Node representing a {@link Result}.
+ */
+ public static class ResultNode extends DefaultMutableTreeNode implements SearchResultsNode {
+ public ResultNode(Result result) {
+ super(result);
+ }
+
+ @Override
+ public String getURL() {
+ return getSearchResult().getUrl();
+ }
+
+ @Override
+ public Result getSearchResult() {
+ return (Result) getUserObject();
+ }
+ }
+
+ private static class CellRenderer extends ColoredTreeCellRenderer {
+ @Override
+ public void customizeCellRenderer(JTree tree, Object value,
+ boolean selected, boolean expanded, boolean leaf,
+ int row, boolean hasFocus) {
+
+ if (value instanceof LayoutNode) {
+ LayoutNode node = (LayoutNode) value;
+ setIcon(node.icon);
+
+ if (node.prefix != null) {
+ append(node.prefix, SimpleTextAttributes.GRAYED_ATTRIBUTES, false);
+ }
+
+ SimpleTextAttributes defaultAttributes = SimpleTextAttributes.REGULAR_ATTRIBUTES;
+ if (node.isHeading) {
+ defaultAttributes = SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES;
+ }
+ append(node.getText(), defaultAttributes, true);
+
+ if (node.suffix != null) {
+ append(" " + node.suffix, SUFFIX_ATTRIBUTES, false);
+ }
+ if (node.count >= 0) {
+ String format = " (%d result" + (node.count != 1 ? "s" : "") + ")";
+ append(String.format(format, node.count), SUFFIX_ATTRIBUTES, false);
+ }
+ return;
+ }
+
+ if (value instanceof RepoNode) {
+ RepoNode node = (RepoNode) value;
+ setIcon(node.getIcon());
+
+ String prefix = node.getPrefix();
+ if (prefix != null) {
+ append(prefix, SimpleTextAttributes.GRAYED_ATTRIBUTES, false);
+ }
+
+ append(node.toString(), SimpleTextAttributes.REGULAR_ATTRIBUTES, true);
+
+ String suffix = node.getSuffix();
+ if (suffix != null) {
+ append(" " + suffix, SimpleTextAttributes.GRAYED_ATTRIBUTES, false);
+ }
+
+ return;
+ }
+
+ if (value instanceof ResultNode) {
+ ResultNode node = (ResultNode) value;
+ Result result = node.getSearchResult();
+
+ if (!hasCode(result)) {
+ setIcon(AllIcons.Actions.CreateFromUsage); // "light bulb" icon
+ append(result.getText());
+ return;
+ }
+
+ setIcon(AllIcons.Actions.EditSource);
+
+ String baseName = ResultUtils.getBaseName(result.getCode().getPath());
+ append(baseName, SimpleTextAttributes.REGULAR_ATTRIBUTES, true);
+
+ int count = result.getCode().getLines().size();
+ String format = " (%d result" + (count != 1 ? "s" : "") + ")";
+ append(String.format(format, count), SUFFIX_ATTRIBUTES, false);
+ return;
+ }
+
+ // This should never happen, but just in case IntelliJ gives us random nodes, then be sure
+ // to always render something.
+ append(value.toString());
+ }
+ }
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/SearchResultsView.java b/src/com/google/devrel/cluestick/studioclient/SearchResultsView.java
new file mode 100644
index 0000000..284fe0a
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/SearchResultsView.java
@@ -0,0 +1,300 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import com.google.devrel.cluestick.searchservice.CluestickSearch;
+import com.google.devrel.cluestick.searchservice.EventLog;
+
+import com.appspot.cluestick_server.search.model.Result;
+import com.intellij.CommonBundle;
+import com.intellij.icons.AllIcons;
+import com.intellij.ide.IdeBundle;
+import com.intellij.openapi.Disposable;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.ActionPlaces;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.DefaultActionGroup;
+import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.diagnostic.Logger;
+import com.intellij.openapi.project.DumbAware;
+import com.intellij.openapi.project.Project;
+import com.intellij.openapi.ui.Splitter;
+import com.intellij.openapi.util.text.StringUtil;
+import com.intellij.ui.DoubleClickListener;
+import com.intellij.ui.OnePixelSplitter;
+import com.intellij.ui.PopupHandler;
+import com.intellij.ui.ScrollPaneFactory;
+import com.intellij.ui.SmartExpander;
+import com.intellij.util.ui.tree.TreeUtil;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.awt.BorderLayout;
+import java.awt.Cursor;
+import java.awt.Desktop;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+/**
+ * SearchResultsView extends JPanel to show the entire Cluestick sample widget.
+ */
+public class SearchResultsView extends JPanel implements Disposable {
+
+ private static final Logger LOG = Logger
+ .getInstance("#com.google.devrel.cluestick.studioclient.SearchResultsView");
+
+ private final EventLog eventLog;
+ private final Splitter splitter;
+ private final SearchResultsTree tree;
+ private final Browser browser;
+ private final JComponent secondComponent;
+ private Runnable closeAction;
+
+ public SearchResultsView(@NotNull final Project project, @NotNull final Symbol symbol,
+ @NotNull final List<Result> results) {
+ eventLog = new EventLog(ServiceManager.getService(CluestickSearch.class));
+
+ setLayout(new BorderLayout());
+
+ tree = new SearchResultsTree(symbol, results);
+ tree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
+ @Override
+ public void valueChanged(TreeSelectionEvent e) {
+ syncBrowser();
+ }
+ });
+ SmartExpander.installOn(tree);
+ browser = new CodeBrowser(project, eventLog);
+
+ splitter = new OnePixelSplitter(false, 0.5f);
+ splitter.setFirstComponent(ScrollPaneFactory.createScrollPane(tree));
+
+ secondComponent = ScrollPaneFactory.createScrollPane(browser.getPanel());
+ splitter.setSecondComponent(secondComponent);
+ secondComponent.setVisible(false); // initially hide browser
+
+ add(splitter, BorderLayout.CENTER);
+
+ createActionsPanel();
+
+ // Add action listeners. These listeners don't do anything if the target node isn't a leaf,
+ // because trunk nodes respond by opening/closing on these same events. Users should use the
+ // popup menu to open any relevant URLs instead.
+
+ new DoubleClickListener() {
+ @Override
+ protected boolean onDoubleClick(MouseEvent event) {
+ TreeNode node = getSingleSelectedNode();
+ TreePath path = tree.getClosestPathForLocation(event.getX(), event.getY());
+ if (node != null && node.isLeaf() && path != null && tree.isPathSelected(path)) {
+ invokeSelectAction(node);
+ return true;
+ }
+ return false;
+ }
+ }.installOn(tree);
+
+ tree.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ TreeNode node = getSingleSelectedNode();
+ if (node != null && node.isLeaf() && e.getKeyCode() == KeyEvent.VK_ENTER) {
+ invokeSelectAction(node);
+ }
+ }
+ });
+
+ DefaultActionGroup actionGroup = new DefaultActionGroup();
+ actionGroup.add(new OpenURLAction());
+ actionGroup.add(new ProvideFeedbackAction());
+ ActionManager actionManager = ActionManager.getInstance();
+ PopupHandler.installPopupHandler(tree, actionGroup, ActionPlaces.USAGE_VIEW_POPUP, actionManager);
+
+ TreeUtil.expandAll(tree);
+ }
+
+ /**
+ * @param close The code to run when the Close button is pressed.
+ */
+ public void setClose(Runnable close) {
+ this.closeAction = close;
+ }
+
+ private void invokeSelectAction(@Nullable TreeNode node) {
+ String url = getURLForNode(node);
+ if (url != null) {
+ openExternalBrowser(url);
+ }
+ }
+
+ @Nullable private String getURLForNode(@Nullable TreeNode node) {
+ if (node instanceof SearchResultsNode) {
+ String url = ((SearchResultsNode) node).getURL();
+ if (!StringUtil.isEmpty(url)) {
+ return url;
+ }
+ }
+ return null;
+ }
+
+ private void openExternalBrowser(@NotNull String url) {
+ LOG.info(String.format("Opening URL: `%s`", url));
+
+ // In nearly all cases, this will load the user's browser with the target URL. However,
+ // swallow its exceptions for sanity.
+ try {
+ Desktop.getDesktop().browse(new URI(url));
+ } catch (URISyntaxException ignored) {
+ // Thrown if the URL is bad.
+ } catch (IOException ignored) {
+ // Thrown if a browser can't be found or run.
+ } catch (RuntimeException ignored) {
+ // Thrown on a variety of other reasons, see-
+ // http://docs.oracle.com/javase/7/docs/api/java/awt/Desktop.html#browse(java.net.URI)
+ }
+ }
+
+ /**
+ * Create and add a JPanel containing default actions. This just includes Close for now.
+ */
+ private void createActionsPanel() {
+ DefaultActionGroup group = new DefaultActionGroup();
+ group.add(new CloseAction());
+
+ ActionManager actionManager = ActionManager.getInstance();
+ JComponent actionsToolbar = actionManager
+ .createActionToolbar(ActionPlaces.CODE_INSPECTION, group, false).getComponent();
+
+ JPanel actionsPanel = new JPanel(new BorderLayout());
+ actionsPanel.add(actionsToolbar, BorderLayout.WEST);
+ add(actionsPanel, BorderLayout.WEST);
+ }
+
+ @Override
+ public void dispose() {
+ splitter.dispose();
+ eventLog.cancel();
+
+ if (browser instanceof Disposable) {
+ ((Disposable) browser).dispose();
+ }
+ }
+
+ private TreeNode getSingleSelectedNode() {
+ TreeSelectionModel selectionModel = tree.getSelectionModel();
+ if (selectionModel.getSelectionCount() == 1) {
+ TreePath pathSelected = tree.getSelectionModel().getLeadSelectionPath();
+ if (pathSelected != null) {
+ return (TreeNode) pathSelected.getLastPathComponent();
+ }
+ }
+ return null;
+ }
+
+ private void syncBrowser() {
+ boolean visible = false;
+ TreeNode node = getSingleSelectedNode();
+ if (node instanceof SearchResultsNode) {
+ Result result = ((SearchResultsNode) node).getSearchResult();
+ if (result != null && showInBrowser(result)) {
+ visible = true;
+ }
+ }
+
+ if (!visible) {
+ browser.showEmpty();
+ }
+ if (secondComponent.isVisible() != visible) {
+ secondComponent.setVisible(visible);
+ splitter.doLayout();
+ splitter.doLayout(); // run twice, in case skipNextLayouting() was called on Splitter
+ }
+ }
+
+ /**
+ * Shows the given {@link Result} inside the current {@link Browser}.
+ * @param result The result to show.
+ * @return Whether the result could be shown.
+ */
+ private boolean showInBrowser(@NotNull Result result) {
+ // TODO(thorogood): Make this part of Browser interface.
+ if (result.getCode() == null) {
+ return false;
+ }
+
+ Cursor currentCursor = getCursor();
+ setCursor(new Cursor(Cursor.WAIT_CURSOR));
+ browser.showResult(result);
+ setCursor(currentCursor);
+ return true;
+ }
+
+ private class OpenURLAction extends AnAction {
+ private OpenURLAction() {
+ super(IdeBundle.message("open.url.in.browser.tooltip"), null, null);
+ }
+
+ @Override
+ public void update(AnActionEvent e) {
+ String url = getURLForNode(getSingleSelectedNode());
+ e.getPresentation().setVisible(url != null);
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ invokeSelectAction(getSingleSelectedNode());
+ }
+ }
+
+ private class ProvideFeedbackAction extends AnAction implements DumbAware {
+ private ProvideFeedbackAction() {
+ super("Provide feedback", null, IconFetcher.GoogleDevelopers);
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ openExternalBrowser("https://goto.google.com/cluestick-feedback");
+ }
+ }
+
+ private class CloseAction extends AnAction implements DumbAware {
+ private CloseAction() {
+ super(CommonBundle.message("action.close"), null, AllIcons.Actions.Cancel);
+ }
+
+ @Override
+ public void actionPerformed(AnActionEvent e) {
+ if (closeAction != null) {
+ closeAction.run();
+ }
+ }
+ }
+}
diff --git a/src/com/google/devrel/cluestick/studioclient/Symbol.java b/src/com/google/devrel/cluestick/studioclient/Symbol.java
new file mode 100644
index 0000000..5c6160e
--- /dev/null
+++ b/src/com/google/devrel/cluestick/studioclient/Symbol.java
@@ -0,0 +1,66 @@
+/*
+ * 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.devrel.cluestick.studioclient;
+
+import java.util.regex.Pattern;
+
+/**
+ * Entity class that represents a Java symbol.
+ */
+public class Symbol {
+
+ // Java packages consist of several segments broken by a period. Each segment must not be empty,
+ // must not start with a digit, and must otherwise only contain [a-z0-9_].
+ // Vaguely inferred from: https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html
+ private static final Pattern PACKAGE_PATTERN = Pattern.compile("^([a-z_][a-z0-9_]*\\.)*");
+
+ /** E.g. java.util.ArrayList */
+ public final String qualifiedName;
+
+ /** E.g. java.util */
+ public final String packageName;
+
+ /** E.g. ArrayList */
+ public final String symbolName;
+
+ public Symbol(String qualifiedName) {
+ this.qualifiedName = qualifiedName;
+
+ // Assume from the first occurrence of a character not included as package name characters,
+ // class name starts.
+ String[] splits = PACKAGE_PATTERN.split(qualifiedName);
+ if (splits.length > 1) {
+ this.symbolName = splits[1];
+ int packageLength = qualifiedName.length() - this.symbolName.length() - 1;
+ this.packageName = qualifiedName.substring(0, packageLength);
+ } else {
+ this.packageName = "";
+ this.symbolName = qualifiedName;
+ }
+ }
+
+ /**
+ * @return Whether this {@link Symbol} is qualified.
+ */
+ public boolean isQualified() {
+ return !packageName.isEmpty();
+ }
+
+ @Override
+ public String toString() {
+ return String.format("{Symbol: %s, Package: %s}", symbolName, packageName);
+ }
+}
diff --git a/src/resources/com/google/devrel/cluestick/studioclient/developers.png b/src/resources/com/google/devrel/cluestick/studioclient/developers.png
new file mode 100644
index 0000000..b9910a7
--- /dev/null
+++ b/src/resources/com/google/devrel/cluestick/studioclient/developers.png
Binary files differ
diff --git a/src/resources/com/google/devrel/cluestick/studioclient/developers_2x.png b/src/resources/com/google/devrel/cluestick/studioclient/developers_2x.png
new file mode 100644
index 0000000..3fe737f
--- /dev/null
+++ b/src/resources/com/google/devrel/cluestick/studioclient/developers_2x.png
Binary files differ