Skip to content

Commit

Permalink
Completion "hangs" with no response
Browse files Browse the repository at this point in the history
Fixes eclipse#743

Signed-off-by: azerr <azerr@redhat.com>
  • Loading branch information
angelozerr committed Aug 8, 2023
1 parent 55ce45f commit f534f4e
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*******************************************************************************
* Copyright (c) 2303 Red Hat Inc. and others.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Angelo ZERR (Red Hat Inc.) - initial implementation
*******************************************************************************/
package org.eclipse.lsp4e.internal;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;

import org.eclipse.lsp4j.jsonrpc.CancelChecker;

/**
* LSP cancellation support hosts the list of LSP requests to cancel when a
* process is canceled (ex: when completion is re-triggered, when hover is give
* up, etc)
*
* @see <a href=
* "https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#cancelRequest">https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#cancelRequest</a>
*/
public class CancellationSupport implements CancelChecker {

private final List<CompletableFuture<?>> futuresToCancel;

private boolean cancelled;

public CancellationSupport() {
this.futuresToCancel = new ArrayList<CompletableFuture<?>>();
this.cancelled = false;
}

public <T> CompletableFuture<T> execute(CompletableFuture<T> future) {
this.futuresToCancel.add(future);
return future;
}

/**
* Cancel all LSP requests.
*/
public void cancel() {
this.cancelled = true;
for (CompletableFuture<?> futureToCancel : futuresToCancel) {
futureToCancel.cancel(true);
}
futuresToCancel.clear();
}

@Override
public void checkCanceled() {
// When LSP requests are called (ex : 'textDocument/completion') the LSP
// response
// items are used to compose some UI item (ex : LSP CompletionItem are translate
// to Eclipse ICompletionProposal).
// If the cancel occurs after the call of those LSP requests, the component
// which uses the LSP responses
// can call checkCanceled to stop the UI creation.
if (cancelled) {
throw new CancellationException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
*******************************************************************************/
package org.eclipse.lsp4e.internal;

import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;

import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseError;
Expand All @@ -23,16 +25,14 @@ private CancellationUtil() {
// this class shouldn't be instantiated
}

public static boolean isRequestCancelledException(final Throwable throwable) {
if (throwable instanceof final CompletionException completionException) {
Throwable cause = completionException.getCause();
if (cause instanceof final ResponseErrorException responseErrorException) {
return isRequestCancelled(responseErrorException);
}
} else if (throwable instanceof ResponseErrorException responseErrorException) {
public static boolean isRequestCancelledException(Throwable throwable) {
if (throwable instanceof CompletionException | throwable instanceof ExecutionException) {
throwable = throwable.getCause();
}
if (throwable instanceof ResponseErrorException responseErrorException) {
return isRequestCancelled(responseErrorException);
}
return false;
return throwable instanceof CancellationException;
}

private static boolean isRequestCancelled(ResponseErrorException responseErrorException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.eclipse.lsp4e.LanguageServerPlugin;
import org.eclipse.lsp4e.LanguageServerWrapper;
import org.eclipse.lsp4e.LanguageServers;
import org.eclipse.lsp4e.internal.CancellationSupport;
import org.eclipse.lsp4e.internal.CancellationUtil;
import org.eclipse.lsp4e.ui.Messages;
import org.eclipse.lsp4e.ui.UI;
Expand All @@ -55,6 +56,7 @@
import org.eclipse.lsp4j.SignatureHelpOptions;
import org.eclipse.lsp4j.SignatureHelpParams;
import org.eclipse.lsp4j.SignatureInformation;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.eclipse.ui.texteditor.ITextEditor;
Expand All @@ -75,12 +77,16 @@ public class LSContentAssistProcessor implements IContentAssistProcessor {
private final Object contextTriggerCharsSemaphore = new Object();
private char[] contextTriggerChars = new char[0];

// The cancellation support used to cancel previous LSP requests 'textDocument/completion' when completion is retriggered
private CancellationSupport cancellationSupport;

public LSContentAssistProcessor() {
this(true);
}

public LSContentAssistProcessor(boolean errorAsCompletionItem) {
this.errorAsCompletionItem = errorAsCompletionItem;
this.cancellationSupport = new CancellationSupport();
}

private final Comparator<LSCompletionProposal> proposalComparator = new LSCompletionProposalComparator();
Expand All @@ -107,12 +113,24 @@ public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int

List<ICompletionProposal> proposals = Collections.synchronizedList(new ArrayList<>());
try {
// Cancel the previous LSP requests 'textDocument/completions' and completionLanguageServersFuture
this.cancellationSupport.cancel();

// Initialize a new cancel support to register:
// - LSP requests 'textDocument/completions'
// - completionLanguageServersFuture
CancellationSupport cancellationSupport = new CancellationSupport();
this.completionLanguageServersFuture = LanguageServers.forDocument(document)
.withFilter(capabilities -> capabilities.getCompletionProvider() != null)
.collectAll((w, ls) -> ls.getTextDocumentService().completion(param)
.thenAccept(completion -> proposals.addAll(toProposals(document, offset, completion, w))));
.withFilter(capabilities -> capabilities.getCompletionProvider() != null) //
.collectAll((w, ls) -> cancellationSupport.execute(ls.getTextDocumentService().completion(param)) //
.thenAccept(completion -> proposals
.addAll(toProposals(document, offset, completion, w, cancellationSupport))));
cancellationSupport.execute(completionLanguageServersFuture);
this.cancellationSupport = cancellationSupport;

// Wait for the result of all LSP requests 'textDocument/completions', this future will be canceled with the next completion
this.completionLanguageServersFuture.get();
} catch (ResponseErrorException | ExecutionException e) {
} catch (ResponseErrorException | ExecutionException | CancellationException e) {
if (!CancellationUtil.isRequestCancelledException(e)) { // do not report error if the server has cancelled the request
LanguageServerPlugin.logError(e);
}
Expand Down Expand Up @@ -203,17 +221,24 @@ private void initiateLanguageServers() {
}
}
private static List<ICompletionProposal> toProposals(IDocument document,
int offset, Either<List<CompletionItem>, CompletionList> completionList, LanguageServerWrapper languageServerWrapper) {
int offset, Either<List<CompletionItem>, CompletionList> completionList, LanguageServerWrapper languageServerWrapper, CancelChecker cancelChecker) {
if (completionList == null) {
return Collections.emptyList();
}
//Stop the compute of ICompletionProposal if the completion has been cancelled
cancelChecker.checkCanceled();
CompletionItemDefaults defaults = completionList.map(o -> null, CompletionList::getItemDefaults);
List<CompletionItem> items = completionList.isLeft() ? completionList.getLeft() : completionList.getRight().getItems();
boolean isIncomplete = completionList.isRight() ? completionList.getRight().isIncomplete() : false;
return items.stream() //
.filter(Objects::nonNull)
.map(item -> new LSCompletionProposal(document, offset, item, defaults,
languageServerWrapper, isIncomplete))
.filter(proposal -> {
//Stop the compute of ICompletionProposal if the completion has been cancelled
cancelChecker.checkCanceled();
return true;
})
.filter(proposal -> proposal.validate(document, offset, null))
.map(ICompletionProposal.class::cast)
.toList();
Expand Down

0 comments on commit f534f4e

Please sign in to comment.