diff options
author | Sam Thorogood <thorogood@google.com> | 2016-02-19 15:29:32 +1100 |
---|---|---|
committer | Sam Thorogood <thorogood@google.com> | 2016-02-19 15:29:32 +1100 |
commit | db6ed59e8576fe47e72c870bd66e4d6e9e016899 (patch) | |
tree | 18333bfca4ef57ab045c364427535a254906d7d1 | |
parent | 1df0580bda965544c9d8258a6dd252fa279e13e3 (diff) | |
download | samples-db6ed59e8576fe47e72c870bd66e4d6e9e016899.tar.gz |
Initial checkin
Change-Id: Id923dacba7e3e7193b90542be25ce540e824f00d
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("&", "&").replace(">", ">").replace("<", "<"); + } + +} 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 Binary files differnew file mode 100644 index 0000000..b9910a7 --- /dev/null +++ b/src/resources/com/google/devrel/cluestick/studioclient/developers.png diff --git a/src/resources/com/google/devrel/cluestick/studioclient/developers_2x.png b/src/resources/com/google/devrel/cluestick/studioclient/developers_2x.png Binary files differnew file mode 100644 index 0000000..3fe737f --- /dev/null +++ b/src/resources/com/google/devrel/cluestick/studioclient/developers_2x.png |