From 89ee243a5cfb4cc7fd59cba07996fbad1a43408e Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Sun, 28 Feb 2021 17:18:40 +0100 Subject: [PATCH] switched completion handling to use CompletableFuture instead of reactor Mono and added cancel handling --- .../completion/VscodeCompletionEngine.java | 9 ++- .../VscodeCompletionEngineAdapter.java | 68 ++++++++++++------- .../util/CompletionHandler.java | 7 +- .../util/CompletionResolveHandler.java | 5 +- .../util/SimpleTextDocumentService.java | 19 +++--- .../ConcourseLanguageServerInitializer.java | 16 ++--- 6 files changed, 73 insertions(+), 51 deletions(-) diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngine.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngine.java index c3f54190d..07a6a65c4 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngine.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngine.java @@ -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 getCompletions(TextDocumentPositionParams params); - CompletionItem resolveCompletion(CompletionItem unresolved); + CompletionList getCompletions(CancelChecker cancelToken, TextDocumentPositionParams params); + CompletionItem resolveCompletion(CancelChecker cancelToken, CompletionItem unresolved); } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java index 8892a90cd..70beff7c3 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/completion/VscodeCompletionEngineAdapter.java @@ -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 resolver = resolvers.get(id instanceof JsonPrimitive ? ((JsonPrimitive)id).getAsString() : id); @@ -122,39 +121,49 @@ public class VscodeCompletionEngineAdapter implements VscodeCompletionEngine { } @Override - public Mono getCompletions(TextDocumentPositionParams params) { - return getCompletionsMono(params); - } - - private Mono 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 completions = filter(engine.getCompletions(doc, offset)); + // get completions + Collection rawCompletions = engine.getCompletions(doc, offset); + + cancelToken.checkCanceled(); + + List completions = filter(rawCompletions); Collections.sort(completions, ScoreableProposal.COMPARATOR); - - CompletionList list = new CompletionList(); + + cancelToken.checkCanceled(); + list.setIsIncomplete(false); List 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; } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionHandler.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionHandler.java index 222d75063..0145e5dfe 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionHandler.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionHandler.java @@ -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 handle(TextDocumentPositionParams params); + CompletionList handle(CancelChecker cancelToken, TextDocumentPositionParams params); } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionResolveHandler.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionResolveHandler.java index e28411346..aed008c1e 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionResolveHandler.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/CompletionResolveHandler.java @@ -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; } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java index 5f9113076..a43d9c133 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java @@ -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, CompletionList>> completion(CompletionParams position) { log.info("completion request arrived: " + position.getTextDocument().getUri()); - CompletionHandler h = completionHandler; - if (h != null) { - return completionHandler.handle(position) - .map(Either::, 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 { diff --git a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerInitializer.java b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerInitializer.java index 70cee70d1..7aa9ded0d 100644 --- a/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerInitializer.java +++ b/headless-services/concourse-language-server/src/main/java/org/springframework/ide/vscode/concourse/ConcourseLanguageServerInitializer.java @@ -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 {