switched completion handling to use CompletableFuture instead of reactor Mono and added cancel handling

This commit is contained in:
Martin Lippert
2021-02-28 17:18:40 +01:00
parent ad505668c1
commit 89ee243a5c
6 changed files with 73 additions and 51 deletions

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016-2018 Pivotal, Inc.
* Copyright (c) 2016, 2021 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -14,14 +14,13 @@ package org.springframework.ide.vscode.commons.languageserver.completion;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import reactor.core.publisher.Mono;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
/**
* Interface that needs to be implemented by a 'completion engine' which can be easily
* wired-up to provide completions for a Vscode language server.
*/
public interface VscodeCompletionEngine {
Mono<CompletionList> getCompletions(TextDocumentPositionParams params);
CompletionItem resolveCompletion(CompletionItem unresolved);
CompletionList getCompletions(CancelChecker cancelToken, TextDocumentPositionParams params);
CompletionItem resolveCompletion(CancelChecker cancelToken, CompletionItem unresolved);
}

View File

@@ -17,6 +17,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CancellationException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
@@ -28,6 +29,7 @@ import org.eclipse.lsp4j.MarkupKind;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import org.eclipse.lsp4j.TextEdit;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits.TextReplace;
@@ -43,9 +45,6 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonPrimitive;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
/**
* Adapts a {@link ICompletionEngine}, wrapping it, to implement {@link VscodeCompletionEngine}
*/
@@ -77,7 +76,7 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
return id;
}
public synchronized void resolveNow(CompletionItem unresolved) {
public synchronized void resolveNow(CancelChecker cancelToken, CompletionItem unresolved) {
Object id = unresolved.getData();
if (id!=null) {
Consumer<CompletionItem> resolver = resolvers.get(id instanceof JsonPrimitive ? ((JsonPrimitive)id).getAsString() : id);
@@ -122,39 +121,49 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
}
@Override
public Mono<CompletionList> getCompletions(TextDocumentPositionParams params) {
return getCompletionsMono(params);
}
private Mono<CompletionList> getCompletionsMono(TextDocumentPositionParams params) {
public CompletionList getCompletions(CancelChecker cancelToken, TextDocumentPositionParams params) {
SimpleTextDocumentService documents = server.getTextDocumentService();
TextDocument doc = documents.getLatestSnapshot(params);
if (doc != null) {
CompletionList list = new CompletionList();
return Mono.fromCallable(() -> {
try {
log.info("Starting completion handling");
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();
}
cancelToken.checkCanceled();
//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
// only standard Java API such as Executor and CompletableFuture directly.
int offset = doc.toOffset(params.getPosition());
List<ICompletionProposal> completions = filter(engine.getCompletions(doc, offset));
// get completions
Collection<ICompletionProposal> rawCompletions = engine.getCompletions(doc, offset);
cancelToken.checkCanceled();
List<ICompletionProposal> completions = filter(rawCompletions);
Collections.sort(completions, ScoreableProposal.COMPARATOR);
CompletionList list = new CompletionList();
cancelToken.checkCanceled();
list.setIsIncomplete(false);
List<CompletionItem> items = new ArrayList<>(completions.size());
SortKeys sortkeys = new SortKeys();
int count = 0;
for (ICompletionProposal c : completions) {
count++;
if (maxCompletions > 0 && count>maxCompletions) {
list.setIsIncomplete(true);
break;
@@ -165,21 +174,32 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
log.error("error computing completion", e);
}
}
cancelToken.checkCanceled();
list.setItems(items);
//This is a hack is no longer needed but keeping it as a reference:
// See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=535823
// Reason hack is not needed is because of the fix in: https://www.pivotaltracker.com/story/show/159667257
// if (LspClient.currentClient()==Client.ECLIPSE) {
// list.setIsIncomplete(true);
// }
// if (LspClient.currentClient()==Client.ECLIPSE) {
// list.setIsIncomplete(true);
// }
return list;
})
.doOnNext(x -> log.info("Got {} completions", x.getItems().size()))
.doAfterTerminate(() -> log.info("Completion handling terminated!"))
.subscribeOn(Schedulers.elastic()); //!!! without this the mono will just be computed on the same thread that calls it.
}
catch (CancellationException e) {
log.info("compututing completions cancellled", e);
throw e;
}
catch (Exception e) {
log.info("error while compututing completions", e);
}
finally {
log.info("Got {} completions", list.getItems().size());
}
}
return Mono.just(SimpleTextDocumentService.NO_COMPLETIONS);
return SimpleTextDocumentService.NO_COMPLETIONS;
}
private CompletionItem adaptItem(TextDocument doc, ICompletionProposal completion, SortKeys sortkeys) throws Exception {
@@ -321,8 +341,8 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine {
}
@Override
public CompletionItem resolveCompletion(CompletionItem unresolved) {
resolver.resolveNow(unresolved);
public CompletionItem resolveCompletion(CancelChecker cancelToken, CompletionItem unresolved) {
resolver.resolveNow(cancelToken, unresolved);
return unresolved;
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016-2018 Pivotal, Inc.
* Copyright (c) 2016, 2021 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -13,10 +13,9 @@ package org.springframework.ide.vscode.commons.languageserver.util;
import org.eclipse.lsp4j.CompletionList;
import org.eclipse.lsp4j.TextDocumentPositionParams;
import reactor.core.publisher.Mono;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
@FunctionalInterface
public interface CompletionHandler {
Mono<CompletionList> handle(TextDocumentPositionParams params);
CompletionList handle(CancelChecker cancelToken, TextDocumentPositionParams params);
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016-2018 Pivotal, Inc.
* Copyright (c) 2016, 2021 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
@@ -12,8 +12,9 @@
package org.springframework.ide.vscode.commons.languageserver.util;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
@FunctionalInterface
public interface CompletionResolveHandler {
CompletionItem handle(CompletionItem unresolved) throws Exception;
CompletionItem handle(CancelChecker cancelToken, CompletionItem unresolved) throws Exception;
}

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.commons.languageserver.util;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -251,13 +252,13 @@ public class SimpleTextDocumentService implements TextDocumentService, DocumentE
public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams position) {
log.info("completion request arrived: " + position.getTextDocument().getUri());
CompletionHandler h = completionHandler;
if (h != null) {
return completionHandler.handle(position)
.map(Either::<List<CompletionItem>, CompletionList>forRight)
.toFuture();
}
return CompletableFuture.completedFuture(Either.forRight(NO_COMPLETIONS));
return CompletableFutures.computeAsync(cancelToken -> {
CompletionHandler h = completionHandler;
if (h != null) {
return Either.forRight(completionHandler.handle(cancelToken, position));
}
return Either.forRight(NO_COMPLETIONS);
});
}
@Override
@@ -269,8 +270,10 @@ public class SimpleTextDocumentService implements TextDocumentService, DocumentE
CompletionResolveHandler h = completionResolveHandler;
if (h != null) {
log.info("Completion item resolve request starting {}", unresolved.getLabel());
return h.handle(unresolved);
return h.handle(cancelToken, unresolved);
}
} catch (CancellationException e) {
throw e;
} catch (Exception e) {
log.warn("exception resolving completion item", e);
} finally {

View File

@@ -45,8 +45,6 @@ import org.springframework.stereotype.Component;
import com.google.common.collect.ImmutableList;
import reactor.core.publisher.Mono;
@Component
public class ConcourseLanguageServerInitializer {
@@ -137,21 +135,23 @@ public class ConcourseLanguageServerInitializer {
// }
// });
documents.onCompletion(params -> {
documents.onCompletion((cancelToken, params) -> {
TextDocument doc = documents.getLatestSnapshot(params);
if (doc != null) {
if (LanguageId.CONCOURSE_PIPELINE.equals(doc.getLanguageId())) {
return forPipelines.completionEngine.getCompletions(params);
return forPipelines.completionEngine.getCompletions(cancelToken, params);
} else if (LanguageId.CONCOURSE_TASK.equals(doc.getLanguageId())) {
return forTasks.completionEngine.getCompletions(params);
return forTasks.completionEngine.getCompletions(cancelToken, params);
}
}
return Mono.just(new CompletionList(false, ImmutableList.of()));
return new CompletionList(false, ImmutableList.of());
});
documents.onCompletionResolve(item -> {
server.completionResolver.resolveNow(item);
documents.onCompletionResolve((cancelToken, item) -> {
server.completionResolver.resolveNow(cancelToken, item);
return item;
});
documents.onHover((cancelToken, params) -> {
log.debug("Concourse hover handler starting");
try {