From b00ebf77275e97f1d9dcf867c7840228d0c1965c Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Sun, 15 Aug 2021 12:48:33 -0700 Subject: [PATCH 01/11] Re-implement the plugin for the Language Server Protocol --- KNOWN_ISSUES.md | 17 +- README.md | 64 +---- build.gradle | 8 +- .../devtools/intellij/ijaas/BaseHandler.java | 69 ----- .../intellij/ijaas/CompletionProducer.java | 124 +++++++++ .../intellij/ijaas/DiagnosticsProducer.java | 141 ++++++++++ .../devtools/intellij/ijaas/IjaasHandler.java | 20 -- .../intellij/ijaas/IjaasLspServer.java | 141 ++++++++++ .../devtools/intellij/ijaas/IjaasServer.java | 154 ----------- .../intellij/ijaas/IjaasStartupActivity.java | 29 ++- .../ijaas/IjaasTextDocumentService.java | 51 ++++ .../intellij/ijaas/IjaasWorkspaceService.java | 17 ++ .../intellij/ijaas/MoreWriteAction.java | 43 ++++ .../intellij/ijaas/OpenFileManager.java | 140 ++++++++++ .../intellij/ijaas/handlers/EchoHandler.java | 24 -- .../ijaas/handlers/JavaCompleteHandler.java | 206 --------------- .../JavaGetImportCandidatesHandler.java | 128 ---------- .../ijaas/handlers/JavaSrcUpdateHandler.java | 198 --------------- vim/autoload/ijaas.vim | 240 ------------------ vim/ftplugin/java/ijaas.vim | 23 -- 20 files changed, 704 insertions(+), 1133 deletions(-) delete mode 100644 src/com/google/devtools/intellij/ijaas/BaseHandler.java create mode 100644 src/com/google/devtools/intellij/ijaas/CompletionProducer.java create mode 100644 src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java delete mode 100644 src/com/google/devtools/intellij/ijaas/IjaasHandler.java create mode 100644 src/com/google/devtools/intellij/ijaas/IjaasLspServer.java delete mode 100644 src/com/google/devtools/intellij/ijaas/IjaasServer.java create mode 100644 src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java create mode 100644 src/com/google/devtools/intellij/ijaas/IjaasWorkspaceService.java create mode 100644 src/com/google/devtools/intellij/ijaas/MoreWriteAction.java create mode 100644 src/com/google/devtools/intellij/ijaas/OpenFileManager.java delete mode 100644 src/com/google/devtools/intellij/ijaas/handlers/EchoHandler.java delete mode 100644 src/com/google/devtools/intellij/ijaas/handlers/JavaCompleteHandler.java delete mode 100644 src/com/google/devtools/intellij/ijaas/handlers/JavaGetImportCandidatesHandler.java delete mode 100644 src/com/google/devtools/intellij/ijaas/handlers/JavaSrcUpdateHandler.java delete mode 100644 vim/autoload/ijaas.vim delete mode 100644 vim/ftplugin/java/ijaas.vim diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 270ca9d..ad97b3f 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -1,14 +1,7 @@ # Known Issues -* Autocomplete omits some results when there are many candidates. -* OrganizeImport adds checkerframework's strange `m` class. -* OrganizeImport won't add any static import. -* OrganizeImport somehow can add the same imports. -* Everything is slow. -* The timeout doesn't work well. This is probably because CodeSmellDetector and - inspection tools internally switch to the swing thread and ProgressIndicator - is not chained properly. -* Detected problems are duplicated. -* There might be a resource leak in the IntelliJ side. -* (Maybe this is not this plugin's issue, but) after BufWritePost, sometimes Vim - goes into a strange state that it accepts ex commands only. No redraw. +* I feel like the LSP version is slower than the non-LSP version. +* Code completion won't trigger reliably. +* I want to see the return types in the code completion popup. +* Error diagnostics won't be updated at the first save action. The second save + action does update them. diff --git a/README.md b/README.md index 2ca86b7..adcc770 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,24 @@ # IntelliJ as a Service -Make IntelliJ as a Java server that does autocompletion for Vim. - -This is not an official Google product (i.e. a 20% project). +Make IntelliJ as a Java LSP server that does autocompletion for Vim. ## Installation 1. git clone. 2. Import project into IntelliJ. Use Gradle plugin. 3. Run `gradle buildPlugin`. It creates `build/distributions/ijaas-*.zip` at the - git root dir. (You can pass `-Pintellij.version=IC-2017.2.6` to specify the - IntelliJ version.) + git root dir. 4. Select "File" menu and click "Settings...". In "Plugins" menu, click "Install plugin from disk..." button. Choose `ijaas-*.zip`. You can uninstall this plugin from this menu. 5. Restart IntelliJ. -6. Add "vim" directory to your runtimepath in Vim in your own way. - (e.g. Plug "$HOME/src/ijaas/vim"). - -## Development - -If you want to isolate your development version and the current version, you -might need two clones. You can load Vim plugins conditionally by using -environment variables. - -``` -if !exists('$USE_DEV_IJAAS') - Plug '$HOME/src/ijaas-dev/vim' -else - Plug '$HOME/src/ijaas/vim' -endif -``` - -You can start another IntelliJ instance by using `gradle runIdea`. You can pass -`-Dijaas.port=5801` to make the testing IntelliJ process listen on a different -port (see https://github.com/JetBrains/gradle-intellij-plugin/issues/18). -Connect to the testing IntelliJ with `USE_DEV_IJAAS=1 IJAAS_PORT=5801 vim`. The -ijaas vim plugin will recognize `IJAAS_PORT` and use that to connect to the -ijaas IntelliJ plugin. - -## Using with ALE - -You can define an ALE linter. -``` -# Disable buf_write_post. Files are checked by ALE. -let g:ijaas_disable_buf_write_post = 1 +## History -# Define ijaas linter. -function! s:ijaas_handle(buffer, lines) abort - let l:response = json_decode(join(a:lines, '\n'))[1] - if has_key(l:response, 'error') || has_key(l:response, 'cause') - return [{ - \ 'lnum': 1, - \ 'text': 'ijaas: RPC error: error=' . l:response['error'] - \ . ' cause=' . l:response['cause'], - \}] - endif +This was started as a 20% project at Google when draftcode was working there. +That's why some files are copyrighted by Google. Now he left the company, and +the repository was moved to his personal account. - return l:response['result']['problems'] -endfunction -call ale#linter#Define('java', { - \ 'name': 'ijaas', - \ 'executable': 'nc', - \ 'command': "echo '[0, {\"method\": \"java_src_update\", \"params\": {\"file\": \"%s\"}}]' | nc localhost 5800 -N", - \ 'lint_file': 1, - \ 'callback': function('s:ijaas_handle'), - \ }) -``` +The initial implementation was written using Vim's channel feature. Then, this +is rewritten to support Language Server Protocol. This version doesn't have a +feature parity, and hence WIP. diff --git a/build.gradle b/build.gradle index 6845fc9..725a5c8 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ plugins { repositories { mavenCentral() + google() } sourceSets { @@ -32,6 +33,9 @@ sourceSets { dependencies { implementation("com.google.guava:guava:30.1.1-jre") implementation("com.google.code.gson:gson:2.8.7") + implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0") + implementation("com.google.dagger:dagger:2.38.1") + annotationProcessor('com.google.dagger:dagger-compiler:2.38.1') } intellij { @@ -45,7 +49,7 @@ patchPluginXml { untilBuild = '212.*' } -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +sourceCompatibility = '11' +targetCompatibility = '11' version '0.1' diff --git a/src/com/google/devtools/intellij/ijaas/BaseHandler.java b/src/com/google/devtools/intellij/ijaas/BaseHandler.java deleted file mode 100644 index 0973627..0000000 --- a/src/com/google/devtools/intellij/ijaas/BaseHandler.java +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.google.devtools.intellij.ijaas; - -import com.google.common.util.concurrent.SettableFuture; -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.intellij.openapi.progress.PerformInBackgroundOption; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.progress.Task; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicReference; - -public abstract class BaseHandler implements IjaasHandler { - protected abstract Class requestClass(); - - protected void validate(ReqT request) {} - - protected abstract ResT handle(ReqT request); - - @Override - public JsonElement handle(JsonElement params) { - SettableFuture ret = SettableFuture.create(); - AtomicReference indicatorRef = new AtomicReference<>(); - ProgressManager.getInstance() - .run( - new Task.Backgroundable( - null, this.getClass().getCanonicalName(), true, PerformInBackgroundOption.DEAF) { - @Override - public void run(ProgressIndicator indicator) { - indicatorRef.set(indicator); - Gson gson = new Gson(); - ReqT request = gson.fromJson(params, requestClass()); - try { - validate(request); - } catch (Exception e) { - ret.setException(new RuntimeException("Validation error", e)); - } - ret.set(gson.toJsonTree(handle(request))); - } - }); - try { - return ret.get(10, TimeUnit.SECONDS); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } catch (TimeoutException | ExecutionException e) { - ProgressIndicator indicator = indicatorRef.get(); - if (indicator != null) { - indicator.cancel(); - } - throw new RuntimeException(e); - } - } -} diff --git a/src/com/google/devtools/intellij/ijaas/CompletionProducer.java b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java new file mode 100644 index 0000000..2474cff --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java @@ -0,0 +1,124 @@ +package com.google.devtools.intellij.ijaas; + +import com.google.devtools.intellij.ijaas.OpenFileManager.OpenedFile; +import com.intellij.codeInsight.completion.CodeCompletionHandlerBase; +import com.intellij.codeInsight.completion.CompletionPhase; +import com.intellij.codeInsight.completion.CompletionProgressIndicator; +import com.intellij.codeInsight.completion.CompletionType; +import com.intellij.codeInsight.completion.impl.CompletionServiceImpl; +import com.intellij.codeInsight.lookup.LookupElement; +import com.intellij.codeInsight.lookup.LookupElementPresentation; +import com.intellij.codeInsight.lookup.impl.LookupImpl; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiKeyword; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiVariable; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; +import javax.inject.Inject; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionItemKind; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +public class CompletionProducer { + private final Project project; + private final ExecutorService executor; + private final OpenFileManager manager; + + @Inject + CompletionProducer(Project project, ExecutorService executor, OpenFileManager manager) { + this.project = project; + this.executor = executor; + this.manager = manager; + } + + CompletableFuture, CompletionList>> completion( + CompletionParams position) { + return CompletableFuture.supplyAsync( + () -> Either.forRight(completionInner(position)), executor); + } + + private CompletionList completionInner(CompletionParams position) { + OpenedFile file = manager.getByURI(position.getTextDocument().getUri()); + Editor editor = file.getEditor(); + WriteAction.runAndWait( + () -> { + editor + .getCaretModel() + .moveToLogicalPosition( + new LogicalPosition( + position.getPosition().getLine(), position.getPosition().getCharacter())); + }); + AtomicReference response = new AtomicReference<>(); + ApplicationManager.getApplication() + .invokeAndWait( + () -> { + try { + CompletionList resp = new CompletionList(); + CompletionHandler handler = new CompletionHandler(); + handler.invokeCompletion(project, editor); + resp.setItems(handler.items); + response.set(resp); + } catch (Exception e) { + e.printStackTrace(); + } + }); + return response.get(); + } + + private static class CompletionHandler extends CodeCompletionHandlerBase { + private List items = new ArrayList<>(); + + private CompletionHandler() { + super(CompletionType.BASIC); + } + + @Override + protected void completionFinished(CompletionProgressIndicator indicator, boolean hasModifiers) { + CompletionServiceImpl.setCompletionPhase(new CompletionPhase.ItemsCalculated(indicator)); + LookupImpl lookup = indicator.getLookup(); + for (LookupElement item : lookup.getItems()) { + PsiElement psi = item.getPsiElement(); + if (psi == null) { + continue; + } + CompletionItem c = new CompletionItem(); + LookupElementPresentation presentation = new LookupElementPresentation(); + item.renderElement(presentation); + c.setLabel(item.getLookupString()); + if (psi instanceof PsiMethod) { + PsiMethod m = (PsiMethod) psi; + if (m.getParameterList().getParametersCount() == 0) { + c.setInsertText(c.getLabel() + "()"); + } else { + c.setInsertText(c.getLabel() + "("); + } + c.setDetail(presentation.getTypeText() + " - " + presentation.getTailText()); + c.setKind(CompletionItemKind.Method); + } else if (psi instanceof PsiKeyword) { + c.setKind(CompletionItemKind.Keyword); + } else if (psi instanceof PsiClass) { + c.setDetail(presentation.getTailText()); + c.setKind(CompletionItemKind.Class); + } else if (psi instanceof PsiVariable) { + c.setDetail(presentation.getTypeText()); + c.setKind(CompletionItemKind.Variable); + } else { + c.setDetail(psi.getClass().getSimpleName()); + } + items.add(c); + } + } + } +} diff --git a/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java b/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java new file mode 100644 index 0000000..d083218 --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java @@ -0,0 +1,141 @@ +package com.google.devtools.intellij.ijaas; + +import com.google.devtools.intellij.ijaas.OpenFileManager.OpenedFile; +import com.intellij.codeInspection.GlobalInspectionContext; +import com.intellij.codeInspection.InspectionEngine; +import com.intellij.codeInspection.InspectionManager; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemHighlightType; +import com.intellij.codeInspection.ex.InspectionToolWrapper; +import com.intellij.codeInspection.ex.Tools; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.util.ProgressIndicatorBase; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.profile.codeInspection.InspectionProfileManager; +import com.intellij.psi.ExternallyAnnotated; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import javax.inject.Inject; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DiagnosticSeverity; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.Endpoint; + +public class DiagnosticsProducer { + private final Project project; + private final ExecutorService executor; + private final Endpoint endpoint; + + @Inject + DiagnosticsProducer(Project project, ExecutorService executor, Endpoint endpoint) { + this.project = project; + this.executor = executor; + this.endpoint = endpoint; + } + + public void updateAsync(OpenedFile file) { + executor.submit( + () -> { + ProgressIndicatorBase indicator = new ProgressIndicatorBase(); + // This is required by AbstractProgressIndicatorBase. + indicator.setIndeterminate(false); + ProgressManager.getInstance().runProcess(() -> updateInternal(file), indicator); + }); + } + + private void updateInternal(OpenedFile file) { + ReadAction.run( + () -> { + try { + // NOTE: CodeSmellInfo is another source for diagnostics. However, it seems that it + // produces the same diagnostics now. Maybe this is wrong, but for now use + // InspectionManager only. Another approach is to use highlights on the editor, but the + // background highlighting process won't run for editors that are not shown. + InspectionManager inspectionManager = InspectionManager.getInstance(project); + GlobalInspectionContext context = inspectionManager.createNewGlobalContext(); + PsiFile psiFile = file.getPsiFile(); + List toolsList = + InspectionProfileManager.getInstance(project) + .getCurrentProfile() + .getAllEnabledInspectionTools(project); + List diagnostics = new ArrayList<>(); + for (Tools tools : toolsList) { + InspectionToolWrapper tool = tools.getInspectionTool(psiFile); + List descs = + InspectionEngine.runInspectionOnFile(psiFile, tool, context); + for (ProblemDescriptor desc : descs) { + // NOTE: There might be other information we can send back. If there's something + // useful, add more. + Diagnostic diag = new Diagnostic(); + diag.setRange(getRange(file, desc)); + diag.setSeverity(getSeverity(desc.getHighlightType())); + diag.setSource(tools.getShortName()); + diag.setMessage(desc.toString()); + diagnostics.add(diag); + } + } + endpoint.notify( + "textDocument/publishDiagnostics", + new PublishDiagnosticsParams(file.getURI(), diagnostics)); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + }); + } + + private static Range getRange(OpenedFile file, ProblemDescriptor desc) { + // NOTE: It's hard to see which PsiElement cannot be null from the IntelliJ source code. Some + // implementation uses ExternallyAnnotated as the TextRange's source. However, those seem to be + // nullable. This conversion code is being overly conservative against the nullness. + PsiElement startElem = desc.getStartElement(); + PsiElement endElem = desc.getEndElement(); + TextRange startRange = null; + TextRange endRange = null; + if (startElem instanceof ExternallyAnnotated) { + startRange = ((ExternallyAnnotated) startElem).getAnnotationRegion(); + } + if (endElem instanceof ExternallyAnnotated) { + endRange = ((ExternallyAnnotated) endElem).getAnnotationRegion(); + } + if (startRange == null) { + startRange = startElem.getTextRange(); + } + if (endRange == null) { + endRange = endElem.getTextRange(); + } + if (endRange == null) { + // Does this happen? Fallback to the start range. + endRange = startRange; + } + if (startRange == null) { + // Does this happen? Fallback to the start of the file. + return new Range(new Position(0, 0), new Position(0, 0)); + } + LogicalPosition startPos = + file.getEditor().offsetToLogicalPosition(startRange.getStartOffset()); + LogicalPosition endPos = file.getEditor().offsetToLogicalPosition(endRange.getEndOffset()); + return new Range( + new Position(startPos.line, startPos.column), new Position(endPos.line, endPos.column)); + } + + private static DiagnosticSeverity getSeverity(ProblemHighlightType type) { + // NOTE: This mapping is an arbitrary choice. + switch (type) { + case ERROR: + case GENERIC_ERROR: + case LIKE_UNKNOWN_SYMBOL: + return DiagnosticSeverity.Error; + default: + return DiagnosticSeverity.Warning; + } + } +} diff --git a/src/com/google/devtools/intellij/ijaas/IjaasHandler.java b/src/com/google/devtools/intellij/ijaas/IjaasHandler.java deleted file mode 100644 index a3f8672..0000000 --- a/src/com/google/devtools/intellij/ijaas/IjaasHandler.java +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.google.devtools.intellij.ijaas; - -import com.google.gson.JsonElement; - -public interface IjaasHandler { - JsonElement handle(JsonElement request); -} diff --git a/src/com/google/devtools/intellij/ijaas/IjaasLspServer.java b/src/com/google/devtools/intellij/ijaas/IjaasLspServer.java new file mode 100644 index 0000000..7a2faf7 --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/IjaasLspServer.java @@ -0,0 +1,141 @@ +package com.google.devtools.intellij.ijaas; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import dagger.Component; +import dagger.Module; +import dagger.Provides; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import javax.inject.Singleton; +import org.eclipse.lsp4j.CompletionOptions; +import org.eclipse.lsp4j.InitializeParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.SaveOptions; +import org.eclipse.lsp4j.ServerCapabilities; +import org.eclipse.lsp4j.ServerInfo; +import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.TextDocumentSyncOptions; +import org.eclipse.lsp4j.jsonrpc.Endpoint; +import org.eclipse.lsp4j.jsonrpc.RemoteEndpoint; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; + +class IjaasLspServer implements LanguageServer { + private final LateBindEndpoint endpoint; + private final Project project; + private final ServerComponent component; + + IjaasLspServer(Project project) { + this.project = project; + this.endpoint = new LateBindEndpoint(); + this.component = + DaggerIjaasLspServer_ServerComponent.builder() + .serverModule(new ServerModule(project, endpoint)) + .build(); + } + + void setRemoteEndpoint(RemoteEndpoint remoteEndpoint) { + endpoint.delegate = remoteEndpoint; + } + + @Override + public CompletableFuture initialize(InitializeParams params) { + // gopls is a good reference on the expected behavior. + // https://github.com/golang/tools/blob/116feaea4581560a370de353120153502e19fc48/internal/lsp/general.go#L120 + ServerCapabilities cap = new ServerCapabilities(); + { + TextDocumentSyncOptions opts = new TextDocumentSyncOptions(); + opts.setOpenClose(true); + opts.setChange(TextDocumentSyncKind.Incremental); + opts.setSave(new SaveOptions(true)); + cap.setTextDocumentSync(opts); + } + { + CompletionOptions opts = new CompletionOptions(); + opts.setTriggerCharacters(List.of(".")); + cap.setCompletionProvider(opts); + } + return CompletableFuture.completedFuture(new InitializeResult(cap, new ServerInfo("ijaas"))); + } + + @Override + public CompletableFuture shutdown() { + Disposer.dispose(component.getDisposable()); + return null; + } + + @Override + public void exit() {} + + @Override + public TextDocumentService getTextDocumentService() { + return component.getTextDocumentService(); + } + + @Override + public WorkspaceService getWorkspaceService() { + return component.getWorkspaceService(); + } + + @Singleton + @Component(modules = {ServerModule.class}) + interface ServerComponent { + IjaasTextDocumentService getTextDocumentService(); + + IjaasWorkspaceService getWorkspaceService(); + + Disposable getDisposable(); + } + + @Module + static class ServerModule { + private final Project project; + private final Endpoint endpoint; + + ServerModule(Project project, Endpoint endpoint) { + this.project = project; + this.endpoint = endpoint; + } + + @Provides + Project provideProject() { + return project; + } + + @Provides + Endpoint provideEndpoint() { + return endpoint; + } + + @Singleton + @Provides + Disposable provideDisposable() { + return Disposer.newDisposable(); + } + + @Singleton + @Provides + ExecutorService provideExecutorService() { + return Executors.newCachedThreadPool(); + } + } + + static class LateBindEndpoint implements Endpoint { + private Endpoint delegate = null; + + @Override + public CompletableFuture request(String method, Object parameter) { + return delegate.request(method, parameter); + } + + @Override + public void notify(String method, Object parameter) { + delegate.notify(method, parameter); + } + } +} diff --git a/src/com/google/devtools/intellij/ijaas/IjaasServer.java b/src/com/google/devtools/intellij/ijaas/IjaasServer.java deleted file mode 100644 index c12f082..0000000 --- a/src/com/google/devtools/intellij/ijaas/IjaasServer.java +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.google.devtools.intellij.ijaas; - -import com.google.common.base.Throwables; -import com.google.devtools.intellij.ijaas.handlers.EchoHandler; -import com.google.devtools.intellij.ijaas.handlers.JavaCompleteHandler; -import com.google.devtools.intellij.ijaas.handlers.JavaGetImportCandidatesHandler; -import com.google.devtools.intellij.ijaas.handlers.JavaSrcUpdateHandler; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonIOException; -import com.google.gson.JsonStreamParser; -import com.google.gson.stream.JsonWriter; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import javax.annotation.Nullable; - -public class IjaasServer { - private final int port; - private final Gson gson = new Gson(); - private final HashMap handlers = new HashMap<>(); - - IjaasServer(int port) { - this.port = port; - // TODO: Add handlers - handlers.put("echo", new EchoHandler()); - handlers.put("java_complete", new JavaCompleteHandler()); - handlers.put("java_src_update", new JavaSrcUpdateHandler()); - handlers.put("java_get_import_candidates", new JavaGetImportCandidatesHandler()); - } - - void start() { - new Thread( - () -> { - ExecutorService executorService = Executors.newCachedThreadPool(); - try (ServerSocket serverSocket = - new ServerSocket(port, 0, InetAddress.getLoopbackAddress())) { - while (true) { - Socket socket = serverSocket.accept(); - executorService.execute(() -> process(socket)); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - }) - .start(); - } - - private void process(Socket socket) { - try { - try { - JsonStreamParser parser = - new JsonStreamParser( - new InputStreamReader( - new BufferedInputStream(socket.getInputStream()), StandardCharsets.UTF_8)); - try (JsonWriter writer = - gson.newJsonWriter( - new OutputStreamWriter( - new BufferedOutputStream(socket.getOutputStream()), StandardCharsets.UTF_8))) { - // There are several top-level values. - writer.setLenient(true); - while (parser.hasNext()) { - JsonArray request = parser.next().getAsJsonArray(); - long id = request.get(0).getAsLong(); - GenericRequest genericRequest = gson.fromJson(request.get(1), GenericRequest.class); - - JsonElement response; - try { - response = gson.toJsonTree(new GenericResponse(processRequest(genericRequest))); - } catch (Exception e) { - response = - gson.toJsonTree( - new ErrorResponse(e.getMessage(), Throwables.getStackTraceAsString(e))); - } - writer.beginArray(); - writer.value(id); - gson.toJson(response, writer); - writer.endArray(); - writer.flush(); - } - } - } finally { - socket.close(); - } - } catch (JsonIOException e) { - Throwable t = e.getCause(); - if (t instanceof EOFException) { - // Ignore. This happens when the input is empty. - } else { - throw new RuntimeException(e); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private JsonElement processRequest(GenericRequest genericRequest) { - if (genericRequest == null) { - throw new RuntimeException("method is required"); - } - IjaasHandler handler = handlers.get(genericRequest.method); - if (handler == null) { - throw new RuntimeException(genericRequest.method + "is not found"); - } - return handler.handle(genericRequest.params); - } - - private static class GenericRequest { - @Nullable String method; - @Nullable JsonElement params; - } - - private static class GenericResponse { - private final JsonElement result; - - GenericResponse(JsonElement result) { - this.result = result; - } - } - - private static class ErrorResponse { - private final String error; - private final String cause; - - ErrorResponse(String error, String cause) { - this.error = error; - this.cause = cause; - } - } -} diff --git a/src/com/google/devtools/intellij/ijaas/IjaasStartupActivity.java b/src/com/google/devtools/intellij/ijaas/IjaasStartupActivity.java index ecf0df8..893bade 100644 --- a/src/com/google/devtools/intellij/ijaas/IjaasStartupActivity.java +++ b/src/com/google/devtools/intellij/ijaas/IjaasStartupActivity.java @@ -2,13 +2,38 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.startup.StartupActivity; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.eclipse.lsp4j.jsonrpc.Launcher; +import org.eclipse.lsp4j.launch.LSPLauncher; import org.jetbrains.annotations.NotNull; public class IjaasStartupActivity implements StartupActivity { @Override public void runActivity(@NotNull Project project) { - IjaasServer server = new IjaasServer(getPort()); - server.start(); + new Thread( + () -> { + ExecutorService executorService = Executors.newCachedThreadPool(); + try (ServerSocket serverSocket = + new ServerSocket(getPort(), 0, InetAddress.getLoopbackAddress())) { + while (true) { + Socket socket = serverSocket.accept(); + IjaasLspServer server = new IjaasLspServer(project); + Launcher launcher = + LSPLauncher.createServerLauncher( + server, socket.getInputStream(), socket.getOutputStream()); + server.setRemoteEndpoint(launcher.getRemoteEndpoint()); + executorService.execute(launcher::startListening); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .start(); } private static int getPort() { diff --git a/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java b/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java new file mode 100644 index 0000000..b70c506 --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java @@ -0,0 +1,51 @@ +package com.google.devtools.intellij.ijaas; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.eclipse.lsp4j.services.TextDocumentService; + +public class IjaasTextDocumentService implements TextDocumentService { + private final OpenFileManager manager; + private final CompletionProducer completionProducer; + + @Inject + IjaasTextDocumentService(OpenFileManager manager, CompletionProducer completionProducer) { + this.manager = manager; + this.completionProducer = completionProducer; + } + + @Override + public CompletableFuture, CompletionList>> completion( + CompletionParams position) { + return completionProducer.completion(position); + } + + @Override + public void didOpen(DidOpenTextDocumentParams params) { + manager.didOpen(params); + } + + @Override + public void didChange(DidChangeTextDocumentParams params) { + manager.didChange(params); + } + + @Override + public void didClose(DidCloseTextDocumentParams params) { + manager.didClose(params); + } + + @Override + public void didSave(DidSaveTextDocumentParams params) { + manager.didSave(params); + } +} diff --git a/src/com/google/devtools/intellij/ijaas/IjaasWorkspaceService.java b/src/com/google/devtools/intellij/ijaas/IjaasWorkspaceService.java new file mode 100644 index 0000000..d4db268 --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/IjaasWorkspaceService.java @@ -0,0 +1,17 @@ +package com.google.devtools.intellij.ijaas; + +import javax.inject.Inject; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.DidChangeWatchedFilesParams; +import org.eclipse.lsp4j.services.WorkspaceService; + +class IjaasWorkspaceService implements WorkspaceService { + @Inject + IjaasWorkspaceService() {} + + @Override + public void didChangeConfiguration(DidChangeConfigurationParams params) {} + + @Override + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {} +} diff --git a/src/com/google/devtools/intellij/ijaas/MoreWriteAction.java b/src/com/google/devtools/intellij/ijaas/MoreWriteAction.java new file mode 100644 index 0000000..121553e --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/MoreWriteAction.java @@ -0,0 +1,43 @@ +package com.google.devtools.intellij.ijaas; + +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.util.ThrowableComputable; +import com.intellij.util.DocumentUtil; +import com.intellij.util.ExceptionUtil; +import java.util.concurrent.atomic.AtomicReference; + +public abstract class MoreWriteAction { + private MoreWriteAction() {} + + public static void runAndWaitForDocument(Runnable r) { + WriteAction.runAndWait( + () -> { + DocumentUtil.writeInRunUndoTransparentAction(r); + }); + } + + public static T computeAndWaitForDocument(ThrowableComputable c) + throws E { + return WriteAction.computeAndWait( + () -> { + AtomicReference result = new AtomicReference<>(); + AtomicReference exception = new AtomicReference<>(); + DocumentUtil.writeInRunUndoTransparentAction( + () -> { + try { + result.set(c.compute()); + } catch (Throwable t) { + exception.set(t); + } + }); + + Throwable t = exception.get(); + if (t != null) { + t.addSuppressed(new RuntimeException()); + ExceptionUtil.rethrowUnchecked(t); + throw (E) t; + } + return result.get(); + }); + } +} diff --git a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java new file mode 100644 index 0000000..108fe9e --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java @@ -0,0 +1,140 @@ +package com.google.devtools.intellij.ijaas; + +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.eclipse.lsp4j.DidChangeTextDocumentParams; +import org.eclipse.lsp4j.DidCloseTextDocumentParams; +import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.TextDocumentContentChangeEvent; + +@Singleton +public class OpenFileManager { + private final Project project; + private final DiagnosticsProducer diagnosticsProducer; + private final Map files = new HashMap<>(); + + @Inject + OpenFileManager(Project project, DiagnosticsProducer diagnosticsProducer) { + this.project = project; + this.diagnosticsProducer = diagnosticsProducer; + } + + @Nullable + public OpenedFile getByPath(String filePath) { + return getByURI("file:/" + filePath); + } + + @Nullable + public OpenedFile getByURI(String uri) { + return files.get(uri); + } + + public void didOpen(DidOpenTextDocumentParams params) { + OpenedFile file = + new OpenedFile( + params.getTextDocument().getUri(), + params.getTextDocument().getVersion(), + params.getTextDocument().getText()); + files.put(params.getTextDocument().getUri(), file); + diagnosticsProducer.updateAsync(file); + } + + public void didChange(DidChangeTextDocumentParams params) { + OpenedFile file = files.get(params.getTextDocument().getUri()); + MoreWriteAction.runAndWaitForDocument( + () -> { + for (TextDocumentContentChangeEvent e : params.getContentChanges()) { + Position startPos = e.getRange().getStart(); + int startOff = + file.editor.logicalPositionToOffset( + new LogicalPosition(startPos.getLine(), startPos.getCharacter())); + Position endPos = e.getRange().getEnd(); + int endOff = + file.editor.logicalPositionToOffset( + new LogicalPosition(endPos.getLine(), endPos.getCharacter())); + file.editor.getDocument().replaceString(startOff, endOff, e.getText()); + } + file.version = params.getTextDocument().getVersion(); + }); + diagnosticsProducer.updateAsync(file); + } + + public void didClose(DidCloseTextDocumentParams params) { + OpenedFile file = files.remove(params.getTextDocument().getUri()); + WriteAction.runAndWait(() -> EditorFactory.getInstance().releaseEditor(file.editor)); + } + + public void didSave(DidSaveTextDocumentParams params) { + OpenedFile file = files.get(params.getTextDocument().getUri()); + MoreWriteAction.runAndWaitForDocument( + () -> { + FileDocumentManager.getInstance().reloadFromDisk(file.editor.getDocument()); + }); + diagnosticsProducer.updateAsync(file); + } + + public class OpenedFile { + private final String uri; + private final Editor editor; + private final PsiFile psiFile; + private int version; + + private OpenedFile(String uri, int version, String text) { + this.uri = uri; + this.version = version; + Pair p = + MoreWriteAction.computeAndWaitForDocument( + () -> { + VirtualFile vf = + LocalFileSystem.getInstance().findFileByPath(uri.replace("file:/", "")); + if (vf == null) { + throw new RuntimeException("Cannot find the VirtualFile"); + } + PsiManager psiManager = PsiManager.getInstance(project); + PsiFile psiFile = psiManager.findFile(vf); + if (psiFile == null) { + throw new RuntimeException("Cannot find the PsiFile"); + } + Document document = PsiDocumentManager.getInstance(project).getDocument(psiFile); + if (document == null) { + throw new RuntimeException("Cannot get the Document"); + } + Editor editor = EditorFactory.getInstance().createEditor(document, project); + editor.getDocument().setText(text); + return Pair.pair(editor, psiFile); + }); + this.editor = p.first; + this.psiFile = p.second; + } + + public String getURI() { + return uri; + } + + public Editor getEditor() { + return editor; + } + + public PsiFile getPsiFile() { + return psiFile; + } + } +} diff --git a/src/com/google/devtools/intellij/ijaas/handlers/EchoHandler.java b/src/com/google/devtools/intellij/ijaas/handlers/EchoHandler.java deleted file mode 100644 index 8190187..0000000 --- a/src/com/google/devtools/intellij/ijaas/handlers/EchoHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.google.devtools.intellij.ijaas.handlers; - -import com.google.devtools.intellij.ijaas.IjaasHandler; -import com.google.gson.JsonElement; - -public class EchoHandler implements IjaasHandler { - @Override - public JsonElement handle(JsonElement request) { - return request; - } -} diff --git a/src/com/google/devtools/intellij/ijaas/handlers/JavaCompleteHandler.java b/src/com/google/devtools/intellij/ijaas/handlers/JavaCompleteHandler.java deleted file mode 100644 index 94789f7..0000000 --- a/src/com/google/devtools/intellij/ijaas/handlers/JavaCompleteHandler.java +++ /dev/null @@ -1,206 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.google.devtools.intellij.ijaas.handlers; - -import com.google.common.collect.Ordering; -import com.google.common.util.concurrent.SettableFuture; -import com.google.devtools.intellij.ijaas.BaseHandler; -import com.google.devtools.intellij.ijaas.handlers.JavaCompleteHandler.Request; -import com.google.devtools.intellij.ijaas.handlers.JavaCompleteHandler.Response; -import com.intellij.codeInsight.completion.CodeCompletionHandlerBase; -import com.intellij.codeInsight.completion.CompletionPhase; -import com.intellij.codeInsight.completion.CompletionProgressIndicator; -import com.intellij.codeInsight.completion.CompletionType; -import com.intellij.codeInsight.completion.impl.CompletionServiceImpl; -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.codeInsight.lookup.LookupElementPresentation; -import com.intellij.codeInsight.lookup.impl.LookupImpl; -import com.intellij.lang.java.JavaLanguage; -import com.intellij.openapi.application.Application; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.CommandProcessor; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.EditorFactory; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectLocator; -import com.intellij.openapi.util.Ref; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiDocumentManager; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiFileFactory; -import com.intellij.psi.PsiKeyword; -import com.intellij.psi.PsiMethod; -import com.intellij.psi.PsiVariable; -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nullable; - -public class JavaCompleteHandler extends BaseHandler { - @Override - protected Class requestClass() { - return Request.class; - } - - @Override - protected Response handle(Request request) { - Project project = findProject(request.file); - if (project == null) { - throw new RuntimeException("Cannot find the target project"); - } - SettableFuture responseFuture = SettableFuture.create(); - Application application = ApplicationManager.getApplication(); - Ref psiFileRef = new Ref<>(); - application.runReadAction( - () -> { - psiFileRef.set( - PsiFileFactory.getInstance(project) - .createFileFromText(JavaLanguage.INSTANCE, request.text)); - }); - PsiFile psiFile = psiFileRef.get(); - - application.invokeAndWait( - () -> { - Editor editor = - EditorFactory.getInstance() - .createEditor( - PsiDocumentManager.getInstance(project).getDocument(psiFile), project); - editor.getCaretModel().moveToOffset(request.offset); - CommandProcessor.getInstance() - .executeCommand( - project, - () -> { - CodeCompletionHandlerBase handler = - new CodeCompletionHandlerBase(CompletionType.BASIC) { - @Override - protected void completionFinished( - CompletionProgressIndicator indicator, boolean hasModifiers) { - CompletionServiceImpl.setCompletionPhase( - new CompletionPhase.ItemsCalculated(indicator)); - Response response = new Response(); - LookupImpl lookup = indicator.getLookup(); - for (LookupElement item : lookup.getItems()) { - PsiElement psi = item.getPsiElement(); - if (psi == null) { - continue; - } - Completion c = new Completion(); - LookupElementPresentation presentation = - new LookupElementPresentation(); - item.renderElement(presentation); - c.word = - item.getLookupString().substring(lookup.getPrefixLength(item)); - if (psi instanceof PsiMethod) { - PsiMethod m = (PsiMethod) psi; - if (m.getParameterList().getParametersCount() == 0) { - c.word += "()"; - } else { - c.word += '('; - } - c.menu = - presentation.getTypeText() + " - " + presentation.getTailText(); - c.kind = Completion.FUNCTION; - } else if (psi instanceof PsiKeyword) { - c.kind = Completion.KEYWORD; - } else if (psi instanceof PsiClass) { - c.menu = presentation.getTailText(); - c.kind = Completion.TYPE; - } else if (psi instanceof PsiVariable) { - c.menu = presentation.getTypeText(); - c.kind = Completion.VARIABLE; - } else { - c.menu = psi.getClass().getSimpleName(); - c.kind = ""; - } - response.completions.add(c); - } - responseFuture.set(response); - } - }; - handler.invokeCompletion(project, editor); - }, - null, - null); - }); - try { - Response response = responseFuture.get(); - Collections.sort(response.completions, new CompletionOrdering()); - return response; - } catch (ExecutionException e) { - throw new RuntimeException(e); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException(e); - } - } - - @Nullable - private Project findProject(String file) { - LocalFileSystem localFileSystem = LocalFileSystem.getInstance(); - ProjectLocator projectLocator = ProjectLocator.getInstance(); - AtomicReference ret = new AtomicReference<>(); - FileUtil.processFilesRecursively( - new File(file), - (f) -> { - VirtualFile vf = localFileSystem.findFileByIoFile(f); - if (vf != null) { - ret.set(projectLocator.guessProjectForFile(vf)); - return false; - } - return true; - }); - return ret.get(); - } - - public static class Request { - String file; - String text; - int offset; - } - - public static class Response { - ArrayList completions = new ArrayList<>(); - } - - public class Completion { - public static final String VARIABLE = "v"; - public static final String FUNCTION = "f"; - public static final String TYPE = "t"; - public static final String KEYWORD = "k"; - - public String word; - public String menu; - public String kind; - } - - private static class CompletionOrdering extends Ordering { - @Override - public int compare(Completion arg0, Completion arg1) { - boolean arg0Keyword = arg0.kind.equals(Completion.KEYWORD); - boolean arg1Keyword = arg1.kind.equals(Completion.KEYWORD); - if (arg0Keyword && !arg1Keyword) { - return 1; - } else if (!arg0Keyword && arg1Keyword) { - return -1; - } - return arg0.word.compareTo(arg1.word); - } - } -} diff --git a/src/com/google/devtools/intellij/ijaas/handlers/JavaGetImportCandidatesHandler.java b/src/com/google/devtools/intellij/ijaas/handlers/JavaGetImportCandidatesHandler.java deleted file mode 100644 index 414cca6..0000000 --- a/src/com/google/devtools/intellij/ijaas/handlers/JavaGetImportCandidatesHandler.java +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.google.devtools.intellij.ijaas.handlers; - -import static java.util.stream.Collectors.toList; - -import com.google.devtools.intellij.ijaas.BaseHandler; -import com.google.devtools.intellij.ijaas.handlers.JavaGetImportCandidatesHandler.Request; -import com.google.devtools.intellij.ijaas.handlers.JavaGetImportCandidatesHandler.Response; -import com.intellij.codeInsight.daemon.impl.quickfix.ImportClassFix; -import com.intellij.lang.java.JavaLanguage; -import com.intellij.openapi.application.Application; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectLocator; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.JavaRecursiveElementWalkingVisitor; -import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiFileFactory; -import com.intellij.psi.PsiJavaCodeReferenceElement; -import com.intellij.psi.PsiJavaFile; -import java.io.File; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nullable; - -public class JavaGetImportCandidatesHandler extends BaseHandler { - @Override - protected Class requestClass() { - return Request.class; - } - - @Override - protected Response handle(Request request) { - Project project = findProject(request.file); - if (project == null) { - throw new RuntimeException("Cannot find the target project"); - } - Application application = ApplicationManager.getApplication(); - Response response = new Response(); - application.runReadAction( - () -> { - PsiFile psiFile = - PsiFileFactory.getInstance(project) - .createFileFromText(JavaLanguage.INSTANCE, request.text); - if (!(psiFile instanceof PsiJavaFile)) { - throw new RuntimeException("Cannot parse as Java file"); - } - PsiJavaFile psiJavaFile = (PsiJavaFile) psiFile; - - Set processed = new HashSet<>(); - for (PsiClass psiClass : psiJavaFile.getClasses()) { - psiClass.accept( - new JavaRecursiveElementWalkingVisitor() { - @Override - public void visitReferenceElement(PsiJavaCodeReferenceElement reference) { - try { - if (reference.getQualifier() != null) { - return; - } - String name = reference.getReferenceName(); - if (processed.contains(name)) { - return; - } - processed.add(name); - - Set candidates = new HashSet<>(); - for (PsiClass t : new ImportClassFix(reference).getClassesToImport()) { - candidates.add(String.format("import %s;", t.getQualifiedName())); - } - if (!candidates.isEmpty()) { - response.choices.add(candidates.stream().sorted().collect(toList())); - } - } finally { - super.visitReferenceElement(reference); - } - } - }); - } - }); - return response; - } - - @Nullable - private Project findProject(String file) { - LocalFileSystem localFileSystem = LocalFileSystem.getInstance(); - ProjectLocator projectLocator = ProjectLocator.getInstance(); - AtomicReference ret = new AtomicReference<>(); - FileUtil.processFilesRecursively( - new File(file), - (f) -> { - VirtualFile vf = localFileSystem.findFileByIoFile(f); - if (vf != null) { - ret.set(projectLocator.guessProjectForFile(vf)); - return false; - } - return true; - }); - return ret.get(); - } - - public static class Request { - String file; - String text; - } - - public static class Response { - List debug = new ArrayList<>(); - List> choices = new ArrayList<>(); - } -} diff --git a/src/com/google/devtools/intellij/ijaas/handlers/JavaSrcUpdateHandler.java b/src/com/google/devtools/intellij/ijaas/handlers/JavaSrcUpdateHandler.java deleted file mode 100644 index e517f5d..0000000 --- a/src/com/google/devtools/intellij/ijaas/handlers/JavaSrcUpdateHandler.java +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2017 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -package com.google.devtools.intellij.ijaas.handlers; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Ordering; -import com.google.devtools.intellij.ijaas.BaseHandler; -import com.google.devtools.intellij.ijaas.handlers.JavaSrcUpdateHandler.Request; -import com.google.devtools.intellij.ijaas.handlers.JavaSrcUpdateHandler.Response; -import com.intellij.codeInsight.CodeSmellInfo; -import com.intellij.codeInspection.GlobalInspectionContext; -import com.intellij.codeInspection.InspectionEngine; -import com.intellij.codeInspection.InspectionManager; -import com.intellij.codeInspection.ProblemDescriptor; -import com.intellij.codeInspection.ProblemHighlightType; -import com.intellij.codeInspection.ex.InspectionToolWrapper; -import com.intellij.codeInspection.ex.Tools; -import com.intellij.lang.annotation.HighlightSeverity; -import com.intellij.openapi.application.Application; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectLocator; -import com.intellij.openapi.util.Ref; -import com.intellij.openapi.util.io.FileUtil; -import com.intellij.openapi.vcs.CodeSmellDetector; -import com.intellij.openapi.vfs.LocalFileSystem; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.profile.codeInspection.InspectionProfileManager; -import com.intellij.psi.PsiFile; -import com.intellij.psi.PsiManager; -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -public class JavaSrcUpdateHandler extends BaseHandler { - @Override - protected Class requestClass() { - return Request.class; - } - - @Override - protected Response handle(Request request) { - File file = new File(FileUtil.toSystemDependentName(request.file)); - if (!file.exists()) { - throw new RuntimeException("Cannot find the file"); - } - Application application = ApplicationManager.getApplication(); - Response response = new Response(); - Ref vfRef = new Ref<>(); - application.invokeAndWait( - () -> { - VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); - if (vf == null) { - throw new RuntimeException("Cannot find the file"); - } - vfRef.set(vf); - }); - VirtualFile vf = vfRef.get(); - - Ref projectRef = new Ref<>(); - Ref psiFileRef = new Ref<>(); - application.runReadAction( - () -> { - Project project = ProjectLocator.getInstance().guessProjectForFile(vf); - if (project == null) { - throw new RuntimeException("Cannot find the target project"); - } - PsiManager psiManager = PsiManager.getInstance(project); - PsiFile psiFile = psiManager.findFile(vf); - if (psiFile == null) { - throw new RuntimeException("Cannot find the PsiFile"); - } - projectRef.set(project); - psiFileRef.set(psiFile); - }); - Project project = projectRef.get(); - PsiFile psiFile = psiFileRef.get(); - - Ref> codeSmellInfosRef = new Ref<>(); - application.invokeAndWait( - () -> { - application.runWriteAction( - () -> { - vf.refresh(false, false); - PsiManager.getInstance(project).reloadFromDisk(psiFile); - }); - codeSmellInfosRef.set( - CodeSmellDetector.getInstance(project).findCodeSmells(ImmutableList.of(vf))); - }); - - application.runReadAction( - () -> { - for (CodeSmellInfo codeSmellInfo : codeSmellInfosRef.get()) { - Problem problem = new Problem(); - problem.lnum = codeSmellInfo.getStartLine() + 1; - problem.text = codeSmellInfo.getDescription(); - problem.type = toProblemType(codeSmellInfo.getSeverity().myVal); - response.problems.add(problem); - } - - InspectionManager inspectionManager = InspectionManager.getInstance(project); - GlobalInspectionContext context = inspectionManager.createNewGlobalContext(false); - - List toolsList = - InspectionProfileManager.getInstance(project) - .getCurrentProfile() - .getAllEnabledInspectionTools(project); - for (Tools tools : toolsList) { - InspectionToolWrapper tool = tools.getInspectionTool(psiFile); - List descs = - InspectionEngine.runInspectionOnFile(psiFile, tool, context); - for (ProblemDescriptor desc : descs) { - Problem problem = new Problem(); - problem.lnum = desc.getLineNumber() + 1; - problem.text = desc.toString(); - problem.type = toProblemType(desc.getHighlightType()); - response.problems.add(problem); - } - } - }); - response.problems.sort(new ProblemOrdering()); - return response; - } - - private static String toProblemType(int severityValue) { - if (severityValue < HighlightSeverity.WARNING.myVal) { - return Problem.INFO; - } else if (severityValue < HighlightSeverity.ERROR.myVal) { - return Problem.WARNING; - } else { - return Problem.ERROR; - } - } - - private static String toProblemType(ProblemHighlightType type) { - switch (type) { - case ERROR: - case GENERIC_ERROR: - case LIKE_UNKNOWN_SYMBOL: - return Problem.ERROR; - default: - return Problem.WARNING; - } - } - - public static class Request { - String file; - } - - public static class Response { - List problems = new ArrayList<>(); - } - - public class Problem { - // Quickfix type characters - // https://github.com/vim/vim/blob/3653822546fb0f1005c32bb5b70dc9bfacdfc954/src/quickfix.c#L2871 - public static final String INFO = "I"; - public static final String WARNING = "W"; - public static final String ERROR = "E"; - - public int lnum; - public String text; - public String type; - } - - private static class ProblemOrdering extends Ordering { - private static final ImmutableMap SEVERITY_ORDER = - ImmutableMap.of( - Problem.INFO, 2, - Problem.WARNING, 1, - Problem.ERROR, 0); - - @Override - public int compare(Problem arg0, Problem arg1) { - int arg0Severity = SEVERITY_ORDER.get(arg0.type); - int arg1Severity = SEVERITY_ORDER.get(arg1.type); - if (arg0Severity != arg1Severity) { - return arg0Severity - arg1Severity; - } - if (arg0.lnum != arg1.lnum) { - return arg0.lnum - arg1.lnum; - } - return arg0.text.compareTo(arg1.text); - } - } -} diff --git a/vim/autoload/ijaas.vim b/vim/autoload/ijaas.vim deleted file mode 100644 index 5b72476..0000000 --- a/vim/autoload/ijaas.vim +++ /dev/null @@ -1,240 +0,0 @@ -" Copyright 2017 Google Inc. -" -" Licensed under the Apache License, Version 2.0 (the "License"); -" you may not use this file except in compliance with the License. -" You may obtain a copy of the License at -" -" https://www.apache.org/licenses/LICENSE-2.0 -" -" Unless required by applicable law or agreed to in writing, software -" distributed under the License is distributed on an "AS IS" BASIS, -" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -" See the License for the specific language governing permissions and -" limitations under the License. -let s:cpo_save = &cpo -set cpo&vim - -if exists("$IJAAS_PORT") - let s:ch = ch_open('localhost:' . $IJAAS_PORT) -else - let s:ch = ch_open('localhost:5800') -endif - -function! ijaas#call(method, params) abort - let l:ci = ch_info(s:ch) - if type(l:ci) != type({}) || l:ci.status != 'open' - throw 'ijaas: Not connected' - endif - let l:response = ch_evalexpr( - \ s:ch, - \ {'method': a:method, 'params': a:params}, - \ {'timeout': 3 * 1000}) - if type(l:response) != type({}) - throw 'ijaas: Timeout' - endif - if has_key(l:response, 'error') || has_key(l:response, 'cause') - if has_key(l:response, 'error') - echo l:response['error'] - endif - if has_key(l:response, 'cause') - for l:line in split(l:response['cause'], "\n") - echom substitute(l:line, " ", ' ', 'g') - endfor - endif - throw 'ijaas: RPC error' - endif - return l:response['result'] -endfunction - -function! ijaas#complete(findstart, base) abort - let l:col = col('.') - 1 - let l:line = getline('.') - while l:col > 0 && l:line[l:col-1] =~# '\a' - let l:col -= 1 - endwhile - if a:findstart - return l:col - endif - - let l:lines = getline(1, '$') - let l:pos = getcurpos() - let l:pos[2] = l:col - if l:pos[1] == 1 " lnum - let l:text = join(l:lines, "\n") - let l:offset = l:pos[2] - 1 " col - else - " Join the lines from beginning to (the cursor line - 1). - let l:text = join(l:lines[0:l:pos[1]-2], "\n") - let l:offset = len(l:text) + 1 + l:pos[2] " \n + col - " Join the rest of the lines. - let l:text .= "\n" . join(l:lines[l:pos[1]-1:], "\n") - endif - - let ret = ijaas#call('java_complete', { - \ 'file': expand('%:p'), - \ 'text': l:text, - \ 'offset': l:offset, - \ })['completions'] - return filter(ret, 'stridx(v:val["word"], a:base) == 0') -endfunction - -function! ijaas#buf_write_post() abort - let l:result = ijaas#call('java_src_update', {'file': expand('%:p')}) - - if !has_key(l:result, 'problems') || len(l:result['problems']) == 0 - call ijaas#set_problems([]) - else - call ijaas#set_problems(l:result['problems']) - end -endfunction - -sign define IjaasErrorSign text=>> texthl=Error -sign define IjaasWarningSign text=>> texthl=Todo - -function! ijaas#set_problems(problems) abort - let l:filename = expand('%:p') - sign unplace * - let l:id = 1 - for l:problem in a:problems - let l:problem['filename'] = l:filename - if l:problem['type'] ==# 'E' - exec 'sign place ' . l:problem['lnum'] . ' line=' . l:problem['lnum'] . ' name=IjaasErrorSign file=' . l:filename - elseif l:problem['type'] ==# 'W' - exec 'sign place ' . l:problem['lnum'] . ' line=' . l:problem['lnum'] . ' name=IjaasWarningSign file=' . l:filename - endif - let l:id += 1 - endfor - call setqflist(a:problems) - cwindow -endfunction - -function! ijaas#organize_import() abort - let l:response = ijaas#call('java_get_import_candidates', { - \ 'file': expand('%:p'), - \ 'text': join(getline(1, '$'), "\n"), - \ }) - - let l:choices = l:response['choices'] - if empty(l:choices) - return - endif - if exists('*fzf#run') - let l:state = { 'question': l:choices } - call s:select_imports_fzf(l:state, "") - else - for l:items in l:choices - let l:sel = s:select_imports_normal(l:items) - if l:sel == "" - " Abort organize import - return - end - call s:add_import(l:sel) - endfor - endif -endfunction - -function! s:select_imports_fzf(state, selected) abort - if a:selected != "" - call s:add_import(a:selected) - endif - while !empty(a:state['question']) - let l:question = a:state['question'][0] - let a:state['question'] = a:state['question'][1:] - if len(l:question) == 1 - call s:add_import(l:question[0]) - else - call fzf#run({ - \ 'source': l:question, - \ 'down': '40%', - \ 'sink': function('s:select_imports_fzf', [a:state]), - \ }) - return - endif - endwhile -endfunction - -function! s:select_imports_normal(inputs) abort - if len(a:inputs) == 1 - return a:inputs[0] - endif - let l:text = "" - let l:index = 1 - for l:item in a:inputs - let l:text .= l:index . ") " . l:item . "\n" - let l:index += 1 - endfor - - let l:selected = input(l:text . "> ") - if l:selected =~# '^\d\+$' - return a:inputs[str2nr(l:selected)-1] - else - return "" - endif -endfunction - -function! s:add_import(input) abort - let l:lnum = 1 - if a:input =~# '^import static ' - let l:last_static_import = 0 - let l:first_import = 0 - - while l:lnum <= line('$') - let l:line = getline(l:lnum) - - if l:line =~# '^import static ' - if a:input <# l:line - call append(l:lnum-1, a:input) - return - endif - let l:last_static_import = l:lnum - elseif l:line =~# '^import ' - let l:first_import = l:lnum - break - endif - let l:lnum += 1 - endwhile - - if l:last_static_import != 0 - call append(l:last_static_import, a:input) - return - elseif l:first_import != 0 - call append(l:first_import-1, [a:input, '']) - return - endif - else - let l:last_import = 0 - - while l:lnum <= line('$') - let l:line = getline(l:lnum) - - if l:line =~# '^import static ' - " Ignore - elseif l:line =~# '^import ' - if a:input <# l:line - call append(l:lnum-1, a:input) - return - endif - let l:last_import = l:lnum - endif - let l:lnum += 1 - endwhile - - if l:last_import != 0 - call append(l:last_import, a:input) - return - endif - endif - - let l:lnum = 1 - while l:lnum <= line('$') - let l:line = getline(l:lnum) - if l:line =~# '^package ' - call append(l:lnum, ['', a:input]) - return - endif - let l:lnum += 1 - endwhile -endfunction - -let &cpo = s:cpo_save -unlet s:cpo_save diff --git a/vim/ftplugin/java/ijaas.vim b/vim/ftplugin/java/ijaas.vim deleted file mode 100644 index f2b7252..0000000 --- a/vim/ftplugin/java/ijaas.vim +++ /dev/null @@ -1,23 +0,0 @@ -" Copyright 2017 Google Inc. -" -" Licensed under the Apache License, Version 2.0 (the "License"); -" you may not use this file except in compliance with the License. -" You may obtain a copy of the License at -" -" https://www.apache.org/licenses/LICENSE-2.0 -" -" Unless required by applicable law or agreed to in writing, software -" distributed under the License is distributed on an "AS IS" BASIS, -" WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -" See the License for the specific language governing permissions and -" limitations under the License. - -if !get(g:, 'ijaas_disable_buf_write_post', 0) - augroup Ijaas - au! * - au BufWritePost call ijaas#buf_write_post() - augroup END -endif - -setlocal omnifunc=ijaas#complete -command! -buffer OrganizeImport call ijaas#organize_import() From 4a79ecb20303db828191c9add8ea9851a554c969 Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Sun, 17 Oct 2021 16:28:38 -0700 Subject: [PATCH 02/11] Implement the definition provider --- build.gradle | 8 ++-- .../intellij/ijaas/IjaasLspServer.java | 1 + .../ijaas/IjaasTextDocumentService.java | 48 +++++++++++++++++++ ...WriteAction.java => MoreWriteActions.java} | 4 +- .../intellij/ijaas/OffsetConverter.java | 40 ++++++++++++++++ .../intellij/ijaas/OpenFileManager.java | 6 +-- 6 files changed, 98 insertions(+), 9 deletions(-) rename src/com/google/devtools/intellij/ijaas/{MoreWriteAction.java => MoreWriteActions.java} (94%) create mode 100644 src/com/google/devtools/intellij/ijaas/OffsetConverter.java diff --git a/build.gradle b/build.gradle index 725a5c8..f7d9551 100644 --- a/build.gradle +++ b/build.gradle @@ -31,11 +31,11 @@ sourceSets { } dependencies { - implementation("com.google.guava:guava:30.1.1-jre") - implementation("com.google.code.gson:gson:2.8.7") + implementation("com.google.guava:guava:31.0.1-jre") + implementation("com.google.code.gson:gson:2.8.8") implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.12.0") - implementation("com.google.dagger:dagger:2.38.1") - annotationProcessor('com.google.dagger:dagger-compiler:2.38.1') + implementation("com.google.dagger:dagger:2.39.1") + annotationProcessor('com.google.dagger:dagger-compiler:2.39.1') } intellij { diff --git a/src/com/google/devtools/intellij/ijaas/IjaasLspServer.java b/src/com/google/devtools/intellij/ijaas/IjaasLspServer.java index 7a2faf7..e5480e3 100644 --- a/src/com/google/devtools/intellij/ijaas/IjaasLspServer.java +++ b/src/com/google/devtools/intellij/ijaas/IjaasLspServer.java @@ -60,6 +60,7 @@ public CompletableFuture initialize(InitializeParams params) { opts.setTriggerCharacters(List.of(".")); cap.setCompletionProvider(opts); } + cap.setDefinitionProvider(true); return CompletableFuture.completedFuture(new InitializeResult(cap, new ServerInfo("ijaas"))); } diff --git a/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java b/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java index b70c506..7262b0d 100644 --- a/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java +++ b/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java @@ -1,15 +1,27 @@ package com.google.devtools.intellij.ijaas; +import com.google.devtools.intellij.ijaas.OpenFileManager.OpenedFile; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import java.io.IOException; import java.util.List; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.TextDocumentService; @@ -29,6 +41,42 @@ public CompletableFuture, CompletionList>> completio return completionProducer.completion(position); } + @Override + public CompletableFuture, List>> + definition(DefinitionParams params) { + OpenedFile of = manager.getByURI(params.getTextDocument().getUri()); + try { + return ReadAction.compute( + () -> { + int offset = + of.getEditor() + .logicalPositionToOffset( + new LogicalPosition( + params.getPosition().getLine(), params.getPosition().getCharacter())); + PsiElement elem; + { + PsiReference ref = of.getPsiFile().findReferenceAt(offset); + if (ref != null) { + elem = ref.resolve().getNavigationElement(); + } else { + elem = of.getPsiFile().findElementAt(offset); + } + } + VirtualFile file = elem.getContainingFile().getViewProvider().getVirtualFile(); + String url = file.getUrl(); + if (url.startsWith("jar:")) { + url = url.replaceFirst("jar:", "zipfile:").replaceFirst("!/", "::"); + } + Position pos = + OffsetConverter.offsetToPosition(file.contentsToByteArray(), elem.getTextOffset()); + Location loc = new Location(url, new Range(pos, pos)); + return CompletableFuture.completedFuture(Either.forLeft(List.of(loc))); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + @Override public void didOpen(DidOpenTextDocumentParams params) { manager.didOpen(params); diff --git a/src/com/google/devtools/intellij/ijaas/MoreWriteAction.java b/src/com/google/devtools/intellij/ijaas/MoreWriteActions.java similarity index 94% rename from src/com/google/devtools/intellij/ijaas/MoreWriteAction.java rename to src/com/google/devtools/intellij/ijaas/MoreWriteActions.java index 121553e..26a1a95 100644 --- a/src/com/google/devtools/intellij/ijaas/MoreWriteAction.java +++ b/src/com/google/devtools/intellij/ijaas/MoreWriteActions.java @@ -6,8 +6,8 @@ import com.intellij.util.ExceptionUtil; import java.util.concurrent.atomic.AtomicReference; -public abstract class MoreWriteAction { - private MoreWriteAction() {} +public abstract class MoreWriteActions { + private MoreWriteActions() {} public static void runAndWaitForDocument(Runnable r) { WriteAction.runAndWait( diff --git a/src/com/google/devtools/intellij/ijaas/OffsetConverter.java b/src/com/google/devtools/intellij/ijaas/OffsetConverter.java new file mode 100644 index 0000000..4d4d91e --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/OffsetConverter.java @@ -0,0 +1,40 @@ +package com.google.devtools.intellij.ijaas; + +import org.eclipse.lsp4j.Position; + +public abstract class OffsetConverter { + private OffsetConverter() {} + + public static int positionToOffset(byte[] bytes, Position pos) { + int line = 0; + int col = 0; + for (int i = 0; i < bytes.length; i++) { + if (line == pos.getLine() && col == pos.getCharacter()) { + return i; + } + + col++; + if (bytes[i] == '\n') { + line++; + col = 0; + } + } + throw new IllegalArgumentException(); + } + + public static Position offsetToPosition(byte[] bytes, int off) { + if (bytes.length < off) { + throw new IllegalArgumentException(); + } + int line = 0; + int col = 0; + for (int i = 0; i < off; i++) { + col++; + if (bytes[i] == '\n') { + line++; + col = 0; + } + } + return new Position(line, col); + } +} diff --git a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java index 108fe9e..c1cdfc0 100644 --- a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java +++ b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java @@ -59,7 +59,7 @@ public void didOpen(DidOpenTextDocumentParams params) { public void didChange(DidChangeTextDocumentParams params) { OpenedFile file = files.get(params.getTextDocument().getUri()); - MoreWriteAction.runAndWaitForDocument( + MoreWriteActions.runAndWaitForDocument( () -> { for (TextDocumentContentChangeEvent e : params.getContentChanges()) { Position startPos = e.getRange().getStart(); @@ -84,7 +84,7 @@ public void didClose(DidCloseTextDocumentParams params) { public void didSave(DidSaveTextDocumentParams params) { OpenedFile file = files.get(params.getTextDocument().getUri()); - MoreWriteAction.runAndWaitForDocument( + MoreWriteActions.runAndWaitForDocument( () -> { FileDocumentManager.getInstance().reloadFromDisk(file.editor.getDocument()); }); @@ -101,7 +101,7 @@ private OpenedFile(String uri, int version, String text) { this.uri = uri; this.version = version; Pair p = - MoreWriteAction.computeAndWaitForDocument( + MoreWriteActions.computeAndWaitForDocument( () -> { VirtualFile vf = LocalFileSystem.getInstance().findFileByPath(uri.replace("file:/", "")); From c59e42fe17bcb7080c629398db786b8b89716ff0 Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Sun, 17 Oct 2021 18:18:33 -0700 Subject: [PATCH 03/11] Add more details to the completion items NB. This causes UI to freeze because it's running on EDT. Better to move out most of the PSI element operations out of EDT. --- .../intellij/ijaas/CompletionProducer.java | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/com/google/devtools/intellij/ijaas/CompletionProducer.java b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java index 2474cff..3f5ba79 100644 --- a/src/com/google/devtools/intellij/ijaas/CompletionProducer.java +++ b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java @@ -19,11 +19,15 @@ import com.intellij.psi.PsiKeyword; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiVariable; +import com.intellij.psi.javadoc.PsiDocComment; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import javax.inject.Inject; import org.eclipse.lsp4j.CompletionItem; import org.eclipse.lsp4j.CompletionItemKind; @@ -77,7 +81,26 @@ private CompletionList completionInner(CompletionParams position) { return response.get(); } + private static String convertSignature( + String returnType, String typeParams, String name, String parameter, String throwTypes) { + StringBuilder b = new StringBuilder(); + if (typeParams != null && !typeParams.isEmpty()) { + b.append(typeParams); + b.append(' '); + } + b.append(returnType); + b.append(' '); + b.append(name); + b.append(parameter); + if (typeParams != null && !throwTypes.isEmpty()) { + b.append(" throws "); + b.append(throwTypes); + } + return b.toString(); + } + private static class CompletionHandler extends CodeCompletionHandlerBase { + private static final Pattern javadocStripRe = Pattern.compile("(\\w*/\\*\\*\\w*|\\w*\\*\\w*)"); private List items = new ArrayList<>(); private CompletionHandler() { @@ -104,7 +127,26 @@ protected void completionFinished(CompletionProgressIndicator indicator, boolean } else { c.setInsertText(c.getLabel() + "("); } - c.setDetail(presentation.getTypeText() + " - " + presentation.getTailText()); + c.setDetail( + convertSignature( + presentation.getTypeText(), + m.getTypeParameterList().getText(), + m.getName(), + presentation.getTailText(), + m.getThrowsList().getText())); + PsiMethod nav = (PsiMethod) m.getNavigationElement(); + PsiDocComment comment = nav.getDocComment(); + if (comment != null) { + String doc = javadocStripRe.matcher(comment.getText()).replaceAll(""); + c.setDocumentation(doc); + } + c.setLabel( + item.getLookupString() + + "(" + + Arrays.stream(m.getParameterList().getParameters()) + .map(p -> p.getName()) + .collect(Collectors.joining(", ")) + + ")"); c.setKind(CompletionItemKind.Method); } else if (psi instanceof PsiKeyword) { c.setKind(CompletionItemKind.Keyword); From b0cdf72f0f82122b92ae168c4e71c3329716160e Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 18 Oct 2021 14:25:25 +0900 Subject: [PATCH 04/11] Handle paths strictly --- .../intellij/ijaas/OpenFileManager.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java index c1cdfc0..dd8753e 100644 --- a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java +++ b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java @@ -13,7 +13,13 @@ import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; +import java.io.File; +import java.net.URI; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.nio.file.Paths; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import javax.annotation.Nullable; import javax.inject.Inject; @@ -39,7 +45,11 @@ public class OpenFileManager { @Nullable public OpenedFile getByPath(String filePath) { - return getByURI("file:/" + filePath); + try { + return getByURI(new File(filePath).toURI().toURL().toExternalForm()); + } catch (MalformedURLException e) { + return null; + } } @Nullable @@ -103,8 +113,15 @@ private OpenedFile(String uri, int version, String text) { Pair p = MoreWriteActions.computeAndWaitForDocument( () -> { + ; + String path = null; + try { + path = Paths.get(new URI(uri)).toFile().getPath(); + } catch(URISyntaxException e) { + throw new RuntimeException("Cannot find the VirtualFile"); + } VirtualFile vf = - LocalFileSystem.getInstance().findFileByPath(uri.replace("file:/", "")); + LocalFileSystem.getInstance().findFileByPath(path); if (vf == null) { throw new RuntimeException("Cannot find the VirtualFile"); } From 70084eb061e1ac89fb849fae3272121235b041d7 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Mon, 18 Oct 2021 14:35:01 +0900 Subject: [PATCH 05/11] Remove needless import --- src/com/google/devtools/intellij/ijaas/OpenFileManager.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java index dd8753e..5f603d9 100644 --- a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java +++ b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java @@ -19,7 +19,6 @@ import java.net.URISyntaxException; import java.nio.file.Paths; import java.util.HashMap; -import java.util.Locale; import java.util.Map; import javax.annotation.Nullable; import javax.inject.Inject; From 890bf73b7f49ee03a6f51c1889619ba8a4e77f91 Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Tue, 19 Oct 2021 09:20:55 -0700 Subject: [PATCH 06/11] Minor fixes * Remove an unused method * Remove unnecessary semicolons * Remove unnecessary initialization * Apply google-java-format --- .../intellij/ijaas/OpenFileManager.java | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java index 5f603d9..16d1468 100644 --- a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java +++ b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java @@ -13,9 +13,7 @@ import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; -import java.io.File; import java.net.URI; -import java.net.MalformedURLException; import java.net.URISyntaxException; import java.nio.file.Paths; import java.util.HashMap; @@ -42,15 +40,6 @@ public class OpenFileManager { this.diagnosticsProducer = diagnosticsProducer; } - @Nullable - public OpenedFile getByPath(String filePath) { - try { - return getByURI(new File(filePath).toURI().toURL().toExternalForm()); - } catch (MalformedURLException e) { - return null; - } - } - @Nullable public OpenedFile getByURI(String uri) { return files.get(uri); @@ -112,15 +101,13 @@ private OpenedFile(String uri, int version, String text) { Pair p = MoreWriteActions.computeAndWaitForDocument( () -> { - ; - String path = null; + String path; try { path = Paths.get(new URI(uri)).toFile().getPath(); - } catch(URISyntaxException e) { + } catch (URISyntaxException e) { throw new RuntimeException("Cannot find the VirtualFile"); } - VirtualFile vf = - LocalFileSystem.getInstance().findFileByPath(path); + VirtualFile vf = LocalFileSystem.getInstance().findFileByPath(path); if (vf == null) { throw new RuntimeException("Cannot find the VirtualFile"); } From 1c052ffca82a1b3b4b2260f1715426ad30786bf4 Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Sat, 13 Nov 2021 13:50:59 -0800 Subject: [PATCH 07/11] Calculate the completion items outside of the EDT The completion calculation code has been producing warnings since it's unnecessarily running on the event dispatch thread. Extract the code and run outside of it. --- .../intellij/ijaas/CompletionProducer.java | 128 ++++++++++-------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/src/com/google/devtools/intellij/ijaas/CompletionProducer.java b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java index 3f5ba79..c7268fc 100644 --- a/src/com/google/devtools/intellij/ijaas/CompletionProducer.java +++ b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java @@ -8,8 +8,8 @@ import com.intellij.codeInsight.completion.impl.CompletionServiceImpl; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementPresentation; -import com.intellij.codeInsight.lookup.impl.LookupImpl; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.LogicalPosition; @@ -36,6 +36,8 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; public class CompletionProducer { + private static final Pattern javadocStripRe = Pattern.compile("(\\w*/\\*\\*\\w*|\\w*\\*\\w*)"); + private final Project project; private final ExecutorService executor; private final OpenFileManager manager; @@ -64,21 +66,80 @@ private CompletionList completionInner(CompletionParams position) { new LogicalPosition( position.getPosition().getLine(), position.getPosition().getCharacter())); }); - AtomicReference response = new AtomicReference<>(); + AtomicReference> elements = new AtomicReference<>(); ApplicationManager.getApplication() .invokeAndWait( () -> { + // Running in EDT. try { - CompletionList resp = new CompletionList(); CompletionHandler handler = new CompletionHandler(); handler.invokeCompletion(project, editor); - resp.setItems(handler.items); - response.set(resp); + elements.set(handler.elements); } catch (Exception e) { e.printStackTrace(); } }); - return response.get(); + return ReadAction.compute( + () -> { + CompletionList resp = new CompletionList(); + resp.setItems(convertItems(elements.get())); + return resp; + }); + } + + private static List convertItems(List elements) { + List items = new ArrayList<>(); + for (LookupElement item : elements) { + PsiElement psi = item.getPsiElement(); + if (psi == null) { + continue; + } + CompletionItem c = new CompletionItem(); + LookupElementPresentation presentation = new LookupElementPresentation(); + item.renderElement(presentation); + c.setLabel(item.getLookupString()); + if (psi instanceof PsiMethod) { + PsiMethod m = (PsiMethod) psi; + if (m.getParameterList().getParametersCount() == 0) { + c.setInsertText(c.getLabel() + "()"); + } else { + c.setInsertText(c.getLabel() + "("); + } + c.setDetail( + convertSignature( + presentation.getTypeText(), + m.getTypeParameterList().getText(), + m.getName(), + presentation.getTailText(), + m.getThrowsList().getText())); + PsiMethod nav = (PsiMethod) m.getNavigationElement(); + PsiDocComment comment = nav.getDocComment(); + if (comment != null) { + String doc = javadocStripRe.matcher(comment.getText()).replaceAll(""); + c.setDocumentation(doc); + } + c.setLabel( + item.getLookupString() + + "(" + + Arrays.stream(m.getParameterList().getParameters()) + .map(p -> p.getName()) + .collect(Collectors.joining(", ")) + + ")"); + c.setKind(CompletionItemKind.Method); + } else if (psi instanceof PsiKeyword) { + c.setKind(CompletionItemKind.Keyword); + } else if (psi instanceof PsiClass) { + c.setDetail(presentation.getTailText()); + c.setKind(CompletionItemKind.Class); + } else if (psi instanceof PsiVariable) { + c.setDetail(presentation.getTypeText()); + c.setKind(CompletionItemKind.Variable); + } else { + c.setDetail(psi.getClass().getSimpleName()); + } + items.add(c); + } + return items; } private static String convertSignature( @@ -100,8 +161,7 @@ private static String convertSignature( } private static class CompletionHandler extends CodeCompletionHandlerBase { - private static final Pattern javadocStripRe = Pattern.compile("(\\w*/\\*\\*\\w*|\\w*\\*\\w*)"); - private List items = new ArrayList<>(); + private List elements = new ArrayList<>(); private CompletionHandler() { super(CompletionType.BASIC); @@ -110,57 +170,7 @@ private CompletionHandler() { @Override protected void completionFinished(CompletionProgressIndicator indicator, boolean hasModifiers) { CompletionServiceImpl.setCompletionPhase(new CompletionPhase.ItemsCalculated(indicator)); - LookupImpl lookup = indicator.getLookup(); - for (LookupElement item : lookup.getItems()) { - PsiElement psi = item.getPsiElement(); - if (psi == null) { - continue; - } - CompletionItem c = new CompletionItem(); - LookupElementPresentation presentation = new LookupElementPresentation(); - item.renderElement(presentation); - c.setLabel(item.getLookupString()); - if (psi instanceof PsiMethod) { - PsiMethod m = (PsiMethod) psi; - if (m.getParameterList().getParametersCount() == 0) { - c.setInsertText(c.getLabel() + "()"); - } else { - c.setInsertText(c.getLabel() + "("); - } - c.setDetail( - convertSignature( - presentation.getTypeText(), - m.getTypeParameterList().getText(), - m.getName(), - presentation.getTailText(), - m.getThrowsList().getText())); - PsiMethod nav = (PsiMethod) m.getNavigationElement(); - PsiDocComment comment = nav.getDocComment(); - if (comment != null) { - String doc = javadocStripRe.matcher(comment.getText()).replaceAll(""); - c.setDocumentation(doc); - } - c.setLabel( - item.getLookupString() - + "(" - + Arrays.stream(m.getParameterList().getParameters()) - .map(p -> p.getName()) - .collect(Collectors.joining(", ")) - + ")"); - c.setKind(CompletionItemKind.Method); - } else if (psi instanceof PsiKeyword) { - c.setKind(CompletionItemKind.Keyword); - } else if (psi instanceof PsiClass) { - c.setDetail(presentation.getTailText()); - c.setKind(CompletionItemKind.Class); - } else if (psi instanceof PsiVariable) { - c.setDetail(presentation.getTypeText()); - c.setKind(CompletionItemKind.Variable); - } else { - c.setDetail(psi.getClass().getSimpleName()); - } - items.add(c); - } + elements.addAll(indicator.getLookup().getItems()); } } } From fc292c7b8287c1cca036b7ae6cd5076002e7f60d Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Sat, 13 Nov 2021 13:54:53 -0800 Subject: [PATCH 08/11] Update the file version properly --- src/com/google/devtools/intellij/ijaas/OpenFileManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java index 16d1468..55ca060 100644 --- a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java +++ b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java @@ -72,6 +72,7 @@ public void didChange(DidChangeTextDocumentParams params) { } file.version = params.getTextDocument().getVersion(); }); + file.version = params.getTextDocument().getVersion(); diagnosticsProducer.updateAsync(file); } From c8dccfa4396042c287f1dcd60e47aefe4247df4f Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Sat, 13 Nov 2021 14:01:03 -0800 Subject: [PATCH 09/11] Extract DefinitionProducer for the uniformity --- .../intellij/ijaas/DefinitionProducer.java | 62 +++++++++++++++++++ .../ijaas/IjaasTextDocumentService.java | 48 +++----------- 2 files changed, 69 insertions(+), 41 deletions(-) create mode 100644 src/com/google/devtools/intellij/ijaas/DefinitionProducer.java diff --git a/src/com/google/devtools/intellij/ijaas/DefinitionProducer.java b/src/com/google/devtools/intellij/ijaas/DefinitionProducer.java new file mode 100644 index 0000000..63fbaca --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/DefinitionProducer.java @@ -0,0 +1,62 @@ +package com.google.devtools.intellij.ijaas; + +import com.google.devtools.intellij.ijaas.OpenFileManager.OpenedFile; +import com.intellij.openapi.application.ReadAction; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiReference; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; +import org.eclipse.lsp4j.DefinitionParams; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + +public class DefinitionProducer { + private final OpenFileManager manager; + + @Inject + DefinitionProducer(OpenFileManager manager) { + this.manager = manager; + } + + CompletableFuture, List>> definition( + DefinitionParams params) { + OpenedFile of = manager.getByURI(params.getTextDocument().getUri()); + try { + return ReadAction.compute( + () -> { + int offset = + of.getEditor() + .logicalPositionToOffset( + new LogicalPosition( + params.getPosition().getLine(), params.getPosition().getCharacter())); + PsiElement elem; + { + PsiReference ref = of.getPsiFile().findReferenceAt(offset); + if (ref != null) { + elem = ref.resolve().getNavigationElement(); + } else { + elem = of.getPsiFile().findElementAt(offset); + } + } + VirtualFile file = elem.getContainingFile().getViewProvider().getVirtualFile(); + String url = file.getUrl(); + if (url.startsWith("jar:")) { + url = url.replaceFirst("jar:", "zipfile:").replaceFirst("!/", "::"); + } + Position pos = + OffsetConverter.offsetToPosition(file.contentsToByteArray(), elem.getTextOffset()); + Location loc = new Location(url, new Range(pos, pos)); + return CompletableFuture.completedFuture(Either.forLeft(List.of(loc))); + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java b/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java index 7262b0d..da197b7 100644 --- a/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java +++ b/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java @@ -1,12 +1,5 @@ package com.google.devtools.intellij.ijaas; -import com.google.devtools.intellij.ijaas.OpenFileManager.OpenedFile; -import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.editor.LogicalPosition; -import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiReference; -import java.io.IOException; import java.util.List; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; @@ -20,19 +13,22 @@ import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.TextDocumentService; public class IjaasTextDocumentService implements TextDocumentService { private final OpenFileManager manager; private final CompletionProducer completionProducer; + private final DefinitionProducer definitionProducer; @Inject - IjaasTextDocumentService(OpenFileManager manager, CompletionProducer completionProducer) { + IjaasTextDocumentService( + OpenFileManager manager, + CompletionProducer completionProducer, + DefinitionProducer definitionProducer) { this.manager = manager; this.completionProducer = completionProducer; + this.definitionProducer = definitionProducer; } @Override @@ -44,37 +40,7 @@ public CompletableFuture, CompletionList>> completio @Override public CompletableFuture, List>> definition(DefinitionParams params) { - OpenedFile of = manager.getByURI(params.getTextDocument().getUri()); - try { - return ReadAction.compute( - () -> { - int offset = - of.getEditor() - .logicalPositionToOffset( - new LogicalPosition( - params.getPosition().getLine(), params.getPosition().getCharacter())); - PsiElement elem; - { - PsiReference ref = of.getPsiFile().findReferenceAt(offset); - if (ref != null) { - elem = ref.resolve().getNavigationElement(); - } else { - elem = of.getPsiFile().findElementAt(offset); - } - } - VirtualFile file = elem.getContainingFile().getViewProvider().getVirtualFile(); - String url = file.getUrl(); - if (url.startsWith("jar:")) { - url = url.replaceFirst("jar:", "zipfile:").replaceFirst("!/", "::"); - } - Position pos = - OffsetConverter.offsetToPosition(file.contentsToByteArray(), elem.getTextOffset()); - Location loc = new Location(url, new Range(pos, pos)); - return CompletableFuture.completedFuture(Either.forLeft(List.of(loc))); - }); - } catch (IOException e) { - throw new RuntimeException(e); - } + return definitionProducer.definition(params); } @Override From 2c55970f9df897ac940edf45fa24faeca169b0cd Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Sat, 13 Nov 2021 14:16:38 -0800 Subject: [PATCH 10/11] Unify the thread controls --- .../intellij/ijaas/CompletionProducer.java | 26 +++---- .../intellij/ijaas/DefinitionProducer.java | 3 +- .../intellij/ijaas/DiagnosticsProducer.java | 3 +- .../intellij/ijaas/OpenFileManager.java | 72 +++++++++---------- ...reWriteActions.java => ThreadControl.java} | 33 +++++++-- 5 files changed, 74 insertions(+), 63 deletions(-) rename src/com/google/devtools/intellij/ijaas/{MoreWriteActions.java => ThreadControl.java} (50%) diff --git a/src/com/google/devtools/intellij/ijaas/CompletionProducer.java b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java index c7268fc..28d9685 100644 --- a/src/com/google/devtools/intellij/ijaas/CompletionProducer.java +++ b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java @@ -8,9 +8,6 @@ import com.intellij.codeInsight.completion.impl.CompletionServiceImpl; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementPresentation; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.application.ReadAction; -import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.project.Project; @@ -25,7 +22,6 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.inject.Inject; @@ -58,7 +54,7 @@ CompletableFuture, CompletionList>> completion( private CompletionList completionInner(CompletionParams position) { OpenedFile file = manager.getByURI(position.getTextDocument().getUri()); Editor editor = file.getEditor(); - WriteAction.runAndWait( + ThreadControl.runOnWriteThread( () -> { editor .getCaretModel() @@ -66,23 +62,17 @@ private CompletionList completionInner(CompletionParams position) { new LogicalPosition( position.getPosition().getLine(), position.getPosition().getCharacter())); }); - AtomicReference> elements = new AtomicReference<>(); - ApplicationManager.getApplication() - .invokeAndWait( + List elements = + ThreadControl.computeOnEDT( () -> { - // Running in EDT. - try { - CompletionHandler handler = new CompletionHandler(); - handler.invokeCompletion(project, editor); - elements.set(handler.elements); - } catch (Exception e) { - e.printStackTrace(); - } + CompletionHandler handler = new CompletionHandler(); + handler.invokeCompletion(project, editor); + return handler.elements; }); - return ReadAction.compute( + return ThreadControl.computeOnReadThread( () -> { CompletionList resp = new CompletionList(); - resp.setItems(convertItems(elements.get())); + resp.setItems(convertItems(elements)); return resp; }); } diff --git a/src/com/google/devtools/intellij/ijaas/DefinitionProducer.java b/src/com/google/devtools/intellij/ijaas/DefinitionProducer.java index 63fbaca..1200cbf 100644 --- a/src/com/google/devtools/intellij/ijaas/DefinitionProducer.java +++ b/src/com/google/devtools/intellij/ijaas/DefinitionProducer.java @@ -1,7 +1,6 @@ package com.google.devtools.intellij.ijaas; import com.google.devtools.intellij.ijaas.OpenFileManager.OpenedFile; -import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; @@ -29,7 +28,7 @@ CompletableFuture, List> DefinitionParams params) { OpenedFile of = manager.getByURI(params.getTextDocument().getUri()); try { - return ReadAction.compute( + return ThreadControl.computeOnReadThread( () -> { int offset = of.getEditor() diff --git a/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java b/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java index d083218..0e785ca 100644 --- a/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java +++ b/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java @@ -8,7 +8,6 @@ import com.intellij.codeInspection.ProblemHighlightType; import com.intellij.codeInspection.ex.InspectionToolWrapper; import com.intellij.codeInspection.ex.Tools; -import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.util.ProgressIndicatorBase; @@ -52,7 +51,7 @@ public void updateAsync(OpenedFile file) { } private void updateInternal(OpenedFile file) { - ReadAction.run( + ThreadControl.runOnReadThread( () -> { try { // NOTE: CodeSmellInfo is another source for diagnostics. However, it seems that it diff --git a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java index 55ca060..5665149 100644 --- a/src/com/google/devtools/intellij/ijaas/OpenFileManager.java +++ b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java @@ -1,6 +1,5 @@ package com.google.devtools.intellij.ijaas; -import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; @@ -46,18 +45,45 @@ public OpenedFile getByURI(String uri) { } public void didOpen(DidOpenTextDocumentParams params) { + Pair p = + ThreadControl.computeOnWriteThreadAndWaitForDocument( + () -> { + String path; + try { + path = Paths.get(new URI(params.getTextDocument().getUri())).toFile().getPath(); + } catch (URISyntaxException e) { + throw new RuntimeException("Cannot find the VirtualFile"); + } + VirtualFile vf = LocalFileSystem.getInstance().findFileByPath(path); + if (vf == null) { + throw new RuntimeException("Cannot find the VirtualFile"); + } + PsiManager psiManager = PsiManager.getInstance(project); + PsiFile psiFile = psiManager.findFile(vf); + if (psiFile == null) { + throw new RuntimeException("Cannot find the PsiFile"); + } + Document document = PsiDocumentManager.getInstance(project).getDocument(psiFile); + if (document == null) { + throw new RuntimeException("Cannot get the Document"); + } + Editor editor = EditorFactory.getInstance().createEditor(document, project); + editor.getDocument().setText(params.getTextDocument().getText()); + return Pair.pair(editor, psiFile); + }); OpenedFile file = new OpenedFile( params.getTextDocument().getUri(), - params.getTextDocument().getVersion(), - params.getTextDocument().getText()); - files.put(params.getTextDocument().getUri(), file); + p.getFirst(), + p.getSecond(), + params.getTextDocument().getVersion()); + files.put(file.uri, file); diagnosticsProducer.updateAsync(file); } public void didChange(DidChangeTextDocumentParams params) { OpenedFile file = files.get(params.getTextDocument().getUri()); - MoreWriteActions.runAndWaitForDocument( + ThreadControl.runOnWriteThreadAndWaitForDocument( () -> { for (TextDocumentContentChangeEvent e : params.getContentChanges()) { Position startPos = e.getRange().getStart(); @@ -78,12 +104,12 @@ public void didChange(DidChangeTextDocumentParams params) { public void didClose(DidCloseTextDocumentParams params) { OpenedFile file = files.remove(params.getTextDocument().getUri()); - WriteAction.runAndWait(() -> EditorFactory.getInstance().releaseEditor(file.editor)); + ThreadControl.runOnWriteThread(() -> EditorFactory.getInstance().releaseEditor(file.editor)); } public void didSave(DidSaveTextDocumentParams params) { OpenedFile file = files.get(params.getTextDocument().getUri()); - MoreWriteActions.runAndWaitForDocument( + ThreadControl.runOnWriteThreadAndWaitForDocument( () -> { FileDocumentManager.getInstance().reloadFromDisk(file.editor.getDocument()); }); @@ -96,37 +122,11 @@ public class OpenedFile { private final PsiFile psiFile; private int version; - private OpenedFile(String uri, int version, String text) { + private OpenedFile(String uri, Editor editor, PsiFile psiFile, int version) { this.uri = uri; + this.editor = editor; + this.psiFile = psiFile; this.version = version; - Pair p = - MoreWriteActions.computeAndWaitForDocument( - () -> { - String path; - try { - path = Paths.get(new URI(uri)).toFile().getPath(); - } catch (URISyntaxException e) { - throw new RuntimeException("Cannot find the VirtualFile"); - } - VirtualFile vf = LocalFileSystem.getInstance().findFileByPath(path); - if (vf == null) { - throw new RuntimeException("Cannot find the VirtualFile"); - } - PsiManager psiManager = PsiManager.getInstance(project); - PsiFile psiFile = psiManager.findFile(vf); - if (psiFile == null) { - throw new RuntimeException("Cannot find the PsiFile"); - } - Document document = PsiDocumentManager.getInstance(project).getDocument(psiFile); - if (document == null) { - throw new RuntimeException("Cannot get the Document"); - } - Editor editor = EditorFactory.getInstance().createEditor(document, project); - editor.getDocument().setText(text); - return Pair.pair(editor, psiFile); - }); - this.editor = p.first; - this.psiFile = p.second; } public String getURI() { diff --git a/src/com/google/devtools/intellij/ijaas/MoreWriteActions.java b/src/com/google/devtools/intellij/ijaas/ThreadControl.java similarity index 50% rename from src/com/google/devtools/intellij/ijaas/MoreWriteActions.java rename to src/com/google/devtools/intellij/ijaas/ThreadControl.java index 26a1a95..6033e4b 100644 --- a/src/com/google/devtools/intellij/ijaas/MoreWriteActions.java +++ b/src/com/google/devtools/intellij/ijaas/ThreadControl.java @@ -1,23 +1,46 @@ package com.google.devtools.intellij.ijaas; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.ReadAction; import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.ThrowableComputable; import com.intellij.util.DocumentUtil; import com.intellij.util.ExceptionUtil; +import com.intellij.util.ThrowableRunnable; import java.util.concurrent.atomic.AtomicReference; -public abstract class MoreWriteActions { - private MoreWriteActions() {} +public abstract class ThreadControl { + private ThreadControl() {} - public static void runAndWaitForDocument(Runnable r) { + public static void runOnReadThread(ThrowableRunnable action) throws E { + ReadAction.run(action); + } + + public static T computeOnReadThread(ThrowableComputable action) + throws E { + return ReadAction.compute(action); + } + + public static T computeOnEDT(Computable action) { + AtomicReference result = new AtomicReference<>(); + ApplicationManager.getApplication().invokeAndWait(() -> result.set(action.compute())); + return result.get(); + } + + public static void runOnWriteThread(ThrowableRunnable action) throws E { + WriteAction.runAndWait(action); + } + + public static void runOnWriteThreadAndWaitForDocument(Runnable r) { WriteAction.runAndWait( () -> { DocumentUtil.writeInRunUndoTransparentAction(r); }); } - public static T computeAndWaitForDocument(ThrowableComputable c) - throws E { + public static T computeOnWriteThreadAndWaitForDocument( + ThrowableComputable c) throws E { return WriteAction.computeAndWait( () -> { AtomicReference result = new AtomicReference<>(); From 876070321b691c657cdf2ca2cf825b90de7d243e Mon Sep 17 00:00:00 2001 From: Masaya Suzuki Date: Sat, 13 Nov 2021 15:04:40 -0800 Subject: [PATCH 11/11] Update a note on the quickfixes --- KNOWN_ISSUES.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index ad97b3f..6dd91ac 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -5,3 +5,25 @@ * I want to see the return types in the code completion popup. * Error diagnostics won't be updated at the first save action. The second save action does update them. + +## publishDiagnostics -> codeAciton -> executeCommand problem + +I tried to expose IntelliJ's code actions via textDocument/codeAction. Based on +`ProblemDescriptor#quickFix`, it seemed like it's just exposing it via +codeAction and actually executing it at executeCommand. + +It turned out that the executeCommand needs to call applyEdit to actually +applying the fix. This means that we need to calculate TextEdit without changing +the actual file. I'm not sure how to do that without triggering the file changes +because the QuickFix interface won't provide a way to do this completely on +memory. LSP doesn't provide a way to change the file on the server side, and +instruct the client to reload the file from the disk (which is understandable). + +I checked the subtypes of QuickFix to see if there's a way to get a preview. +It's possible for certain subtypes. But this means that we need to calculate the +TextEdit out of this preview. It's cumbersome. + +Considering that I've been using only OrganizeImport (finding out the missing +import statements and remove unused import statements), I feel I'm OK without +having a quickfix. It's nice to have feature, but I have no idea how to +implement it without completely changing IntelliJ's QuickFix model.