Workaround for vscode 1.12 completion sorting issue.
This commit is contained in:
@@ -8,13 +8,15 @@
|
||||
* Contributors:
|
||||
* Pivotal, Inc. - initial API and implementation
|
||||
*******************************************************************************/
|
||||
|
||||
package org.springframework.ide.vscode.commons.languageserver.completion;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.lsp4j.CompletionItem;
|
||||
import org.eclipse.lsp4j.CompletionList;
|
||||
@@ -28,6 +30,7 @@ import org.springframework.ide.vscode.commons.languageserver.completion.Document
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
|
||||
import org.springframework.ide.vscode.commons.languageserver.util.SortKeys;
|
||||
import org.springframework.ide.vscode.commons.util.Log;
|
||||
import org.springframework.ide.vscode.commons.util.Renderable;
|
||||
import org.springframework.ide.vscode.commons.util.StringUtil;
|
||||
import org.springframework.ide.vscode.commons.util.text.TextDocument;
|
||||
@@ -40,18 +43,73 @@ import reactor.core.scheduler.Schedulers;
|
||||
*/
|
||||
public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
|
||||
|
||||
public static class LazyCompletionResolver {
|
||||
private int nextId = 0; //Used to assign unique id to completion items.
|
||||
|
||||
private Map<String, Consumer<CompletionItem>> resolvers = new HashMap<>();
|
||||
|
||||
private String nextId() {
|
||||
//Warning: it's tempting to return 'int' and use a Integer object as id but...
|
||||
// Looks like that breaks things because the Integer becomes a Double after being
|
||||
// serialized and deserialized to json.
|
||||
return ""+(nextId++);
|
||||
}
|
||||
|
||||
public synchronized String resolveLater(ICompletionProposal completion, TextDocument doc) {
|
||||
String id = nextId();
|
||||
resolvers.put(id, (unresolved) -> {
|
||||
try {
|
||||
resolveItem(doc, completion, unresolved);
|
||||
} catch (Exception e) {
|
||||
Log.log(e);
|
||||
}
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
public synchronized void resolveNow(CompletionItem unresolved) {
|
||||
Object id = unresolved.getData();
|
||||
if (id!=null) {
|
||||
Consumer<CompletionItem> resolver = resolvers.get(id);
|
||||
if (resolver!=null) {
|
||||
resolver.accept(unresolved);
|
||||
unresolved.setData(null); //No longer needed after item is resolved.
|
||||
Log.info("Resolved completion: "+unresolved);
|
||||
} else {
|
||||
Log.warn("Couldn't resolve completion item. Did it already get flushed from the resolver's cache? "+unresolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clear() {
|
||||
resolvers.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private final static int DEFAULT_MAX_COMPLETIONS = 50;
|
||||
private int maxCompletions = DEFAULT_MAX_COMPLETIONS; //TODO: move this to CompletionEngineOptions.
|
||||
final static Logger logger = LoggerFactory.getLogger(VscodeCompletionEngineAdapter.class);
|
||||
|
||||
private SimpleLanguageServer server;
|
||||
private ICompletionEngine engine;
|
||||
private LazyCompletionResolver resolver = null;
|
||||
|
||||
public VscodeCompletionEngineAdapter(SimpleLanguageServer server, ICompletionEngine engine) {
|
||||
this.server = server;
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* By setting a non-null {@link LazyCompletionResolver} you can enable lazy completion resolution.
|
||||
* By default lazy resolution is not implemented.
|
||||
* <p>
|
||||
* The resolver is injected rather than created locally to allow sharing it between multiple
|
||||
* engines.
|
||||
*/
|
||||
public void setLazyCompletionResolver(LazyCompletionResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
public void setMaxCompletionsNumber(int maxCompletions) {
|
||||
this.maxCompletions = maxCompletions;
|
||||
}
|
||||
@@ -66,6 +124,11 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
|
||||
TextDocument doc = documents.get(params).copy();
|
||||
if (doc!=null) {
|
||||
return Mono.fromCallable(() -> {
|
||||
if (resolver!=null) {
|
||||
//Assumes we don't have more than one completion request in flight from the client.
|
||||
// So when a new request arrives we can forget about the old unresolved items:
|
||||
resolver.clear();
|
||||
}
|
||||
//TODO: This callable is a 'big lump of work' so can't be canceled in pieces.
|
||||
// Should we push using of reactive streams down further and compose this all
|
||||
// using reactive style? If not then this is overkill could just as well use
|
||||
@@ -105,19 +168,27 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
|
||||
item.setSortText(sortkeys.next());
|
||||
item.setFilterText(completion.getFilterText());
|
||||
item.setDetail(completion.getDetail());
|
||||
item.setDocumentation(toMarkdown(completion.getDocumentation()));
|
||||
adaptEdits(item, doc, completion.getTextEdit());
|
||||
if (resolver!=null) {
|
||||
item.setData(resolver.resolveLater(completion, doc));
|
||||
} else {
|
||||
resolveItem(doc, completion, item);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private String toMarkdown(Renderable r) {
|
||||
private static void resolveItem(TextDocument doc, ICompletionProposal completion, CompletionItem item) throws Exception {
|
||||
item.setDocumentation(toMarkdown(completion.getDocumentation()));
|
||||
adaptEdits(item, doc, completion.getTextEdit());
|
||||
}
|
||||
|
||||
private static String toMarkdown(Renderable r) {
|
||||
if (r!=null) {
|
||||
return r.toMarkdown();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void adaptEdits(CompletionItem item, TextDocument doc, DocumentEdits edits) throws Exception {
|
||||
private static void adaptEdits(CompletionItem item, TextDocument doc, DocumentEdits edits) throws Exception {
|
||||
TextReplace replaceEdit = edits.asReplacement(doc);
|
||||
if (replaceEdit==null) {
|
||||
//The original edit does nothing.
|
||||
@@ -134,7 +205,7 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
|
||||
}
|
||||
}
|
||||
|
||||
private String vscodeIndentFix(TextDocument doc, Position start, String newText) {
|
||||
private static String vscodeIndentFix(TextDocument doc, Position start, String newText) {
|
||||
//Vscode applies some magic indent to a multi-line edit text. We do everything ourself so we have adjust for the magic
|
||||
// and do some kind of 'inverse magic' here.
|
||||
//See here: https://github.com/Microsoft/language-server-protocol/issues/83
|
||||
@@ -149,9 +220,7 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<CompletionItem> resolveCompletion(CompletionItem unresolved) {
|
||||
//TODO: item is pre-resoved so we don't do anything, but we really should somehow defer some work, such as
|
||||
// for example computing docs and edits to resolve time.
|
||||
//The tricky part is that we have to probably remember infos about the unresolved elements somehow so we can resolve later.
|
||||
resolver.resolveNow(unresolved);
|
||||
return CompletableFuture.completedFuture(unresolved);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ public abstract class SimpleLanguageServer implements LanguageServer, LanguageCl
|
||||
c.setHoverProvider(true);
|
||||
|
||||
CompletionOptions completionProvider = new CompletionOptions();
|
||||
completionProvider.setResolveProvider(false);
|
||||
completionProvider.setResolveProvider(hasLazyCompletionResolver());
|
||||
c.setCompletionProvider(completionProvider);
|
||||
|
||||
if (hasQuickFixes()) {
|
||||
@@ -233,6 +233,10 @@ public abstract class SimpleLanguageServer implements LanguageServer, LanguageCl
|
||||
return c;
|
||||
}
|
||||
|
||||
public boolean hasLazyCompletionResolver() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean hasDocumentSymbolHandler() {
|
||||
return getTextDocumentService().hasDocumentSymbolHandler();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface YamlCompletionEngineOptions {
|
||||
default boolean includeDeindentedProposals() {
|
||||
//Disabled by default for now because of bug introduced in VSCode 1.12:
|
||||
//https://github.com/Microsoft/vscode/issues/26096
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
YamlCompletionEngineOptions DEFAULT = new YamlCompletionEngineOptions() {};
|
||||
|
||||
@@ -372,6 +372,7 @@ public class Editor {
|
||||
}
|
||||
|
||||
public void apply(CompletionItem completion) throws Exception {
|
||||
completion = harness.resolveCompletionItem(completion);
|
||||
TextEdit edit = completion.getTextEdit();
|
||||
String docText = doc.getText();
|
||||
if (edit!=null) {
|
||||
|
||||
@@ -391,12 +391,15 @@ public class LanguageServerHarness {
|
||||
}
|
||||
|
||||
|
||||
public CompletionItem resolveCompletionItem(CompletionItem unresolved) {
|
||||
try {
|
||||
return server.getTextDocumentService().resolveCompletionItem(unresolved).get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
public CompletionItem resolveCompletionItem(CompletionItem maybeUnresolved) {
|
||||
if (server.hasLazyCompletionResolver()) {
|
||||
try {
|
||||
return server.getTextDocumentService().resolveCompletionItem(maybeUnresolved).get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return maybeUnresolved;
|
||||
}
|
||||
|
||||
public List<CompletionItem> resolveCompletions(CompletionList completions) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.concurrent.CompletableFuture;
|
||||
import org.eclipse.lsp4j.CompletionList;
|
||||
import org.eclipse.lsp4j.DiagnosticSeverity;
|
||||
import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter;
|
||||
import org.springframework.ide.vscode.commons.languageserver.completion.VscodeCompletionEngineAdapter.LazyCompletionResolver;
|
||||
import org.springframework.ide.vscode.commons.languageserver.hover.HoverInfoProvider;
|
||||
import org.springframework.ide.vscode.commons.languageserver.hover.VscodeHoverEngineAdapter;
|
||||
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
|
||||
@@ -51,6 +52,7 @@ public class ConcourseLanguageServer extends SimpleLanguageServer {
|
||||
private SchemaSpecificPieces forPipelines;
|
||||
private SchemaSpecificPieces forTasks;
|
||||
private final YamlQuickfixes yamlQuickfixes;
|
||||
private final LazyCompletionResolver completionResolver = new LazyCompletionResolver(); //Set this to null to disable lazy completion resolving
|
||||
|
||||
private class SchemaSpecificPieces {
|
||||
|
||||
@@ -63,6 +65,7 @@ public class ConcourseLanguageServer extends SimpleLanguageServer {
|
||||
SchemaBasedYamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema);
|
||||
YamlCompletionEngine yamlCompletionEngine = new YamlCompletionEngine(structureProvider, contextProvider, COMPLETION_OPTIONS);
|
||||
this.completionEngine = new VscodeCompletionEngineAdapter(ConcourseLanguageServer.this, yamlCompletionEngine);
|
||||
this.completionEngine.setLazyCompletionResolver(completionResolver);
|
||||
|
||||
HoverInfoProvider infoProvider = new YamlHoverInfoProvider(currentAsts, structureProvider, contextProvider);
|
||||
this.hoverEngine = new VscodeHoverEngineAdapter(ConcourseLanguageServer.this, infoProvider);
|
||||
@@ -80,6 +83,11 @@ public class ConcourseLanguageServer extends SimpleLanguageServer {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLazyCompletionResolver() {
|
||||
return completionResolver!=null;
|
||||
}
|
||||
|
||||
public ConcourseLanguageServer(YamlCompletionEngineOptions completionOptions) {
|
||||
super("vscode-concourse");
|
||||
this.COMPLETION_OPTIONS = completionOptions;
|
||||
@@ -124,10 +132,9 @@ public class ConcourseLanguageServer extends SimpleLanguageServer {
|
||||
}
|
||||
return CompletableFuture.completedFuture(new CompletionList(false, ImmutableList.of()));
|
||||
});
|
||||
documents.onCompletionResolve(params -> {
|
||||
//this is a bogus implementation. But its not currently used.
|
||||
throw new IllegalStateException("Not implemented");
|
||||
|
||||
documents.onCompletionResolve(item -> {
|
||||
completionResolver.resolveNow(item);
|
||||
return CompletableFuture.completedFuture(item);
|
||||
});
|
||||
documents.onHover(params -> {
|
||||
TextDocument doc = documents.get(params);
|
||||
|
||||
@@ -45,7 +45,7 @@ public class ConcourseLanguageServerTest {
|
||||
}
|
||||
|
||||
private void assertExpectedInitResult(InitializeResult initResult) {
|
||||
assertThat(initResult.getCapabilities().getCompletionProvider().getResolveProvider()).isFalse();
|
||||
assertThat(initResult.getCapabilities().getCompletionProvider().getResolveProvider()).isTrue();
|
||||
assertThat(initResult.getCapabilities().getTextDocumentSync().getLeft()).isEqualTo(TextDocumentSyncKind.Incremental);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user