Skip to content
39 changes: 27 additions & 12 deletions KNOWN_ISSUES.md
Original file line number Diff line number Diff line change
@@ -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.
64 changes: 9 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 8 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ plugins {

repositories {
mavenCentral()
google()
}

sourceSets {
Expand All @@ -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 {
Expand All @@ -45,7 +49,7 @@ patchPluginXml {
untilBuild = '212.*'
}

sourceCompatibility = '1.8'
targetCompatibility = '1.8'
sourceCompatibility = '11'
targetCompatibility = '11'

version '0.1'
69 changes: 0 additions & 69 deletions src/com/google/devtools/intellij/ijaas/BaseHandler.java

This file was deleted.

166 changes: 166 additions & 0 deletions src/com/google/devtools/intellij/ijaas/CompletionProducer.java
Original file line number Diff line number Diff line change
@@ -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<Either<List<CompletionItem>, 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<LookupElement> 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<CompletionItem> convertItems(List<LookupElement> elements) {
List<CompletionItem> 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<LookupElement> 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());
}
}
}
Loading