diff --git a/KNOWN_ISSUES.md b/KNOWN_ISSUES.md index 270ca9d..6dd91ac 100644 --- a/KNOWN_ISSUES.md +++ b/KNOWN_ISSUES.md @@ -1,14 +1,29 @@ # 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. + +## 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. 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..f7d9551 100644 --- a/build.gradle +++ b/build.gradle @@ -20,6 +20,7 @@ plugins { repositories { mavenCentral() + google() } sourceSets { @@ -30,8 +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.39.1") + annotationProcessor('com.google.dagger:dagger-compiler:2.39.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..28d9685 --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/CompletionProducer.java @@ -0,0 +1,166 @@ +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.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 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.regex.Pattern; +import java.util.stream.Collectors; +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 static final Pattern javadocStripRe = Pattern.compile("(\\w*/\\*\\*\\w*|\\w*\\*\\w*)"); + + 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(); + ThreadControl.runOnWriteThread( + () -> { + editor + .getCaretModel() + .moveToLogicalPosition( + new LogicalPosition( + position.getPosition().getLine(), position.getPosition().getCharacter())); + }); + List elements = + ThreadControl.computeOnEDT( + () -> { + CompletionHandler handler = new CompletionHandler(); + handler.invokeCompletion(project, editor); + return handler.elements; + }); + return ThreadControl.computeOnReadThread( + () -> { + CompletionList resp = new CompletionList(); + resp.setItems(convertItems(elements)); + 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( + 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 List elements = new ArrayList<>(); + + private CompletionHandler() { + super(CompletionType.BASIC); + } + + @Override + protected void completionFinished(CompletionProgressIndicator indicator, boolean hasModifiers) { + CompletionServiceImpl.setCompletionPhase(new CompletionPhase.ItemsCalculated(indicator)); + elements.addAll(indicator.getLookup().getItems()); + } + } +} 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..1200cbf --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/DefinitionProducer.java @@ -0,0 +1,61 @@ +package com.google.devtools.intellij.ijaas; + +import com.google.devtools.intellij.ijaas.OpenFileManager.OpenedFile; +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 ThreadControl.computeOnReadThread( + () -> { + 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/DiagnosticsProducer.java b/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java new file mode 100644 index 0000000..0e785ca --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/DiagnosticsProducer.java @@ -0,0 +1,140 @@ +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.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) { + ThreadControl.runOnReadThread( + () -> { + 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..e5480e3 --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/IjaasLspServer.java @@ -0,0 +1,142 @@ +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); + } + cap.setDefinitionProvider(true); + 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..da197b7 --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/IjaasTextDocumentService.java @@ -0,0 +1,65 @@ +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.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.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, + DefinitionProducer definitionProducer) { + this.manager = manager; + this.completionProducer = completionProducer; + this.definitionProducer = definitionProducer; + } + + @Override + public CompletableFuture, CompletionList>> completion( + CompletionParams position) { + return completionProducer.completion(position); + } + + @Override + public CompletableFuture, List>> + definition(DefinitionParams params) { + return definitionProducer.definition(params); + } + + @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/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 new file mode 100644 index 0000000..5665149 --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/OpenFileManager.java @@ -0,0 +1,144 @@ +package com.google.devtools.intellij.ijaas; + +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.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Paths; +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 getByURI(String uri) { + return files.get(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(), + 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()); + ThreadControl.runOnWriteThreadAndWaitForDocument( + () -> { + 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(); + }); + file.version = params.getTextDocument().getVersion(); + diagnosticsProducer.updateAsync(file); + } + + public void didClose(DidCloseTextDocumentParams params) { + OpenedFile file = files.remove(params.getTextDocument().getUri()); + ThreadControl.runOnWriteThread(() -> EditorFactory.getInstance().releaseEditor(file.editor)); + } + + public void didSave(DidSaveTextDocumentParams params) { + OpenedFile file = files.get(params.getTextDocument().getUri()); + ThreadControl.runOnWriteThreadAndWaitForDocument( + () -> { + 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, Editor editor, PsiFile psiFile, int version) { + this.uri = uri; + this.editor = editor; + this.psiFile = psiFile; + this.version = version; + } + + public String getURI() { + return uri; + } + + public Editor getEditor() { + return editor; + } + + public PsiFile getPsiFile() { + return psiFile; + } + } +} diff --git a/src/com/google/devtools/intellij/ijaas/ThreadControl.java b/src/com/google/devtools/intellij/ijaas/ThreadControl.java new file mode 100644 index 0000000..6033e4b --- /dev/null +++ b/src/com/google/devtools/intellij/ijaas/ThreadControl.java @@ -0,0 +1,66 @@ +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 ThreadControl { + private ThreadControl() {} + + 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 computeOnWriteThreadAndWaitForDocument( + 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/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()