From 3fc3d44851a00c7686ba9017e11565037d076b26 Mon Sep 17 00:00:00 2001 From: azerr Date: Tue, 16 Apr 2024 18:26:35 +0200 Subject: [PATCH] fix: Infinite completionItem/resolve when documentation for completion returns nothing Fixes #182 Signed-off-by: azerr --- .../devtools/lsp4ij/LSPFileSupport.java | 13 ++ .../redhat/devtools/lsp4ij/LSPIJUtils.java | 8 -- .../devtools/lsp4ij/LSPRequestConstants.java | 1 + .../features/completion/CompletionData.java | 29 +++++ .../features/completion/CompletionPrefix.java | 5 +- .../completion/LSPCompletionContributor.java | 118 +++++++----------- .../completion/LSPCompletionParams.java | 34 +++++ .../completion/LSPCompletionProposal.java | 97 +++++++++----- .../completion/LSPCompletionSupport.java | 100 +++++++++++++++ .../lsp4ij/internal/SupportedFeatures.java | 6 +- 10 files changed, 298 insertions(+), 113 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionData.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionParams.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionSupport.java diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java index 87dcff9b..eaf650a8 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java @@ -16,6 +16,7 @@ import com.redhat.devtools.lsp4ij.features.codeAction.intention.LSPIntentionCodeActionSupport; import com.redhat.devtools.lsp4ij.features.codeLens.LSPCodeLensSupport; import com.redhat.devtools.lsp4ij.features.color.LSPColorSupport; +import com.redhat.devtools.lsp4ij.features.completion.LSPCompletionSupport; import com.redhat.devtools.lsp4ij.features.documentLink.LSPDocumentLinkSupport; import com.redhat.devtools.lsp4ij.features.documentation.LSPHoverSupport; import com.redhat.devtools.lsp4ij.features.foldingRange.LSPFoldingRangeSupport; @@ -60,6 +61,8 @@ public class LSPFileSupport implements Disposable { private final LSPRenameSupport renameSupport; + private final LSPCompletionSupport completionSupport; + private LSPFileSupport(@NotNull PsiFile file) { this.file = file; this.codeLensSupport = new LSPCodeLensSupport(file); @@ -74,6 +77,7 @@ private LSPFileSupport(@NotNull PsiFile file) { this.intentionCodeActionSupport = new LSPIntentionCodeActionSupport(file); this.prepareRenameSupport = new LSPPrepareRenameSupport(file); this.renameSupport = new LSPRenameSupport(file); + this.completionSupport = new LSPCompletionSupport(file); file.putUserData(LSP_FILE_SUPPORT_KEY, this); } @@ -93,6 +97,7 @@ public void dispose() { getIntentionCodeActionSupport().cancel(); getPrepareRenameSupport().cancel(); getRenameSupport().cancel(); + getCompletionSupport().cancel(); } /** @@ -203,6 +208,14 @@ public LSPRenameSupport getRenameSupport() { return renameSupport; } + /** + * Returns the LSP completion support. + * + * @return the LSP completion support. + */ + public LSPCompletionSupport getCompletionSupport() { + return completionSupport; + } /** * Return the existing LSP file support for the given Psi file, or create a new one if necessary. diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java index debefee4..a6c2a82c 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java @@ -623,14 +623,6 @@ private static void applyWorkspaceEdit(Document document, List edits) return EditorFactory.getInstance().getEditors(document, project); } - public static CompletionParams toCompletionParams(URI fileUri, int offset, Document document) { - Position start = toPosition(offset, document); - CompletionParams param = new CompletionParams(); - param.setPosition(start); - param.setTextDocument(toTextDocumentIdentifier(fileUri)); - return param; - } - public static TextDocumentIdentifier toTextDocumentIdentifier(VirtualFile file) { return toTextDocumentIdentifier(toUri(file)); } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPRequestConstants.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPRequestConstants.java index 1408ecdf..28d07f05 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LSPRequestConstants.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPRequestConstants.java @@ -44,6 +44,7 @@ public class LSPRequestConstants { public static final String TEXT_DOCUMENT_SIGNATURE_HELP = "textDocument/signatureHelp"; public static final String TEXT_DOCUMENT_PREPARE_RENAME = "textDocument/prepareRename"; public static final String TEXT_DOCUMENT_RENAME = "textDocument/rename"; + public static final String TEXT_DOCUMENT_DOCUMENT_COMPLETION = "textDocument/completion"; private LSPRequestConstants() { diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionData.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionData.java new file mode 100644 index 00000000..513671c0 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionData.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.features.completion; + +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.CompletionList; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +/** + * LSP completion data. + * + * @param completion the LSP completion result + * @param languageServer the language server which has created the completion result. + */ +record CompletionData(@NotNull Either, CompletionList> completion, + @NotNull LanguageServerItem languageServer) { +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionPrefix.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionPrefix.java index dc03a04a..a4c8fa0f 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionPrefix.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/CompletionPrefix.java @@ -60,7 +60,8 @@ public Document getDocument() { * @param item the completion item. * @return the proper prefix from the given text range and label/filterText defined in the given completion item and null otherwise. */ - public @Nullable String getPrefixFor(@NotNull Range textEditRange, CompletionItem item) { + public @Nullable String getPrefixFor(@NotNull Range textEditRange, + @NotNull CompletionItem item) { // Try to get the computed prefix from the cache String prefix = prefixCache.get(textEditRange); if (prefix == null && !prefixCache.containsKey(textEditRange)) { @@ -101,7 +102,7 @@ public Document getDocument() { return prefix; } - private static String getAccurateFilterText(CompletionItem item) { + private static String getAccurateFilterText(@NotNull CompletionItem item) { String filterText = item.getFilterText(); if (StringUtils.isBlank(filterText)) { return null; diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionContributor.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionContributor.java index 56dc7c7f..07c872ed 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionContributor.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionContributor.java @@ -14,36 +14,33 @@ import com.intellij.codeInsight.completion.CompletionParameters; import com.intellij.codeInsight.completion.CompletionResultSet; import com.intellij.codeInsight.completion.PrioritizedLookupElement; -import com.intellij.codeInsight.lookup.LookupElement; -import com.intellij.codeInsight.lookup.LookupElementBuilder; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPFileSupport; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LanguageServerItem; import com.redhat.devtools.lsp4ij.LanguageServersRegistry; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; -import com.redhat.devtools.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.lsp4ij.internal.StringUtils; -import com.redhat.devtools.lsp4ij.LSPRequestConstants; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.net.URI; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.concurrent.BlockingDeque; +import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.ExecutionException; + +import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally; +import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone; /** * LSP completion contributor. @@ -65,65 +62,44 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No Editor editor = parameters.getEditor(); Document document = editor.getDocument(); - Project project = psiFile.getProject(); int offset = parameters.getOffset(); - URI uri = LSPIJUtils.toUri(file); - - ProgressManager.checkCanceled(); - final CancellationSupport cancellationSupport = new CancellationSupport(); + // Get LSP completion items from cache or create them + LSPCompletionParams params = new LSPCompletionParams(LSPIJUtils.toTextDocumentIdentifier(file), LSPIJUtils.toPosition(offset, document), offset); + CompletableFuture> future = LSPFileSupport.getSupport(psiFile) + .getCompletionSupport() + .getCompletions(params); try { - CompletableFuture> completionLanguageServersFuture = initiateLanguageServers(file, project); - cancellationSupport.execute(completionLanguageServersFuture); - ProgressManager.checkCanceled(); - - /* - process the responses out of the completable loop as it may cause deadlock if user is typing - more characters as toProposals will require as read lock that this thread already have and - async processing is occuring on a separate thread. - */ - CompletionParams params = LSPIJUtils.toCompletionParams(uri, offset, document); - BlockingDeque, CompletionList>, LanguageServerItem>> proposals = new LinkedBlockingDeque<>(); - - CompletableFuture future = completionLanguageServersFuture - .thenComposeAsync(languageServers -> cancellationSupport.execute( - CompletableFuture.allOf(languageServers.stream() - .map(languageServer -> - cancellationSupport.execute(languageServer.getServer() - .getTextDocumentService() - .completion(params), languageServer, LSPRequestConstants.TEXT_DOCUMENT_COMPLETION) - .thenAcceptAsync(completion -> { - if (completion != null) { - proposals.add(new Pair<>(completion, languageServer)); - } - })) - .toArray(CompletableFuture[]::new)))); + // Wait upon the future is finished and stop the wait if there are some ProcessCanceledException. + waitUntilDone(future, psiFile); + } catch (CancellationException | ProcessCanceledException e) { + return; + } catch (ExecutionException e) { + LOGGER.error("Error while consuming LSP 'textDocument/completion' request", e); + return; + } - ProgressManager.checkCanceled(); - while (!future.isDone() || !proposals.isEmpty()) { - ProgressManager.checkCanceled(); - Pair, CompletionList>, LanguageServerItem> pair = proposals.poll(25, TimeUnit.MILLISECONDS); - if (pair != null) { - Either, CompletionList> completion = pair.getFirst(); - if (completion != null) { - CompletionPrefix completionPrefix = new CompletionPrefix(offset, document); - addCompletionItems(psiFile, editor, completionPrefix, pair.getFirst(), pair.getSecond(), result, cancellationSupport); - } + ProgressManager.checkCanceled(); + if (isDoneNormally(future)) { + List data = future.getNow(Collections.emptyList()); + if (!data.isEmpty()) { + CompletionPrefix completionPrefix = new CompletionPrefix(offset, document); + for(var item : data) { + ProgressManager.checkCanceled(); + addCompletionItems(psiFile, editor, completionPrefix, item.completion(), item.languageServer(), result); } } - } catch (ProcessCanceledException cancellation) { - cancellationSupport.cancel(); - throw cancellation; - } catch (RuntimeException | InterruptedException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - result.addElement(createErrorProposal(offset, e)); } } private static final CompletionItemComparator completionProposalComparator = new CompletionItemComparator(); - private void addCompletionItems(PsiFile file, Editor editor, CompletionPrefix completionPrefix, Either, - CompletionList> completion, LanguageServerItem languageServer, @NotNull CompletionResultSet result, CancellationSupport cancellationSupport) { + private void addCompletionItems(@NotNull PsiFile file, + @NotNull Editor editor, + @NotNull CompletionPrefix completionPrefix, + @NotNull Either, CompletionList> completion, + @NotNull LanguageServerItem languageServer, + @NotNull CompletionResultSet result) { CompletionItemDefaults itemDefaults = null; List items = new ArrayList<>(); if (completion.isLeft()) { @@ -145,7 +121,7 @@ private void addCompletionItems(PsiFile file, Editor editor, CompletionPrefix co // Invalid completion Item, ignore it continue; } - cancellationSupport.checkCanceled(); + ProgressManager.checkCanceled(); // Create lookup item var lookupItem = createLookupItem(file, editor, completionPrefix.getCompletionOffset(), item, itemDefaults, languageServer); @@ -166,15 +142,19 @@ private void addCompletionItems(PsiFile file, Editor editor, CompletionPrefix co } } - private static LSPCompletionProposal createLookupItem(PsiFile file, Editor editor, int offset, - CompletionItem item, - CompletionItemDefaults itemDefaults, LanguageServerItem languageServer) { + private static LSPCompletionProposal createLookupItem(@NotNull PsiFile file, + @NotNull Editor editor, + int offset, + @NotNull CompletionItem item, + @Nullable CompletionItemDefaults itemDefaults, + @NotNull LanguageServerItem languageServer) { // Update text edit range with item defaults if needed updateWithItemDefaults(item, itemDefaults); return new LSPCompletionProposal(file, editor, offset, item, languageServer); } - private static void updateWithItemDefaults(CompletionItem item, CompletionItemDefaults itemDefaults) { + private static void updateWithItemDefaults(@NotNull CompletionItem item, + @Nullable CompletionItemDefaults itemDefaults) { if (itemDefaults == null) { return; } @@ -194,14 +174,4 @@ private static void updateWithItemDefaults(CompletionItem item, CompletionItemDe } } - - private static LookupElement createErrorProposal(int offset, Exception ex) { - return LookupElementBuilder.create("Error while computing completion", ""); - } - - private static CompletableFuture> initiateLanguageServers(@NotNull VirtualFile file, @NotNull Project project) { - return LanguageServiceAccessor - .getInstance(project) - .getLanguageServers(file,LanguageServerItem::isCompletionSupported); - } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionParams.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionParams.java new file mode 100644 index 00000000..b56b3832 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionParams.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at https://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.features.completion; + +import org.eclipse.lsp4j.CompletionParams; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.TextDocumentIdentifier; + +/** + * LSP completion parameters which hosts the offset where completion has been triggered. + */ +public class LSPCompletionParams extends CompletionParams { + + // Use transient to avoid serializing the fields when GSON will be processed + private transient final int offset; + + public LSPCompletionParams(TextDocumentIdentifier textDocument, Position position, int offset) { + super.setTextDocument(textDocument); + super.setPosition(position); + this.offset = offset; + } + + public int getOffset() { + return offset; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionProposal.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionProposal.java index dd52373b..ccf9a016 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionProposal.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionProposal.java @@ -16,20 +16,22 @@ import com.intellij.codeInsight.completion.InsertionContext; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.codeInsight.lookup.LookupElementPresentation; +import com.intellij.codeInsight.lookup.LookupElementRenderer; import com.intellij.codeInsight.template.Template; import com.intellij.codeInsight.template.TemplateManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorModificationUtil; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LSPIJUtils; import com.redhat.devtools.lsp4ij.LanguageServerItem; -import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; import com.redhat.devtools.lsp4ij.commands.CommandExecutor; -import com.redhat.devtools.lsp4ij.internal.StringUtils; import com.redhat.devtools.lsp4ij.features.completion.snippet.LspSnippetIndentOptions; +import com.redhat.devtools.lsp4ij.internal.StringUtils; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.jetbrains.annotations.NotNull; @@ -40,11 +42,12 @@ import java.net.URI; import java.util.*; import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import static com.redhat.devtools.lsp4ij.features.completion.snippet.LspSnippetVariableConstants.*; +import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally; +import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone; import static com.redhat.devtools.lsp4ij.ui.IconMapper.getIcon; /** @@ -62,7 +65,7 @@ public class LSPCompletionProposal extends LookupElement { private int bestOffset; private final Editor editor; private final LanguageServerItem languageServer; - private String documentation; + private CompletableFuture resolvedCompletionItemFuture; public LSPCompletionProposal(PsiFile file, Editor editor, int offset, CompletionItem item, LanguageServerItem languageServer) { this.file = file; @@ -246,6 +249,25 @@ public void renderElement(LookupElementPresentation presentation) { } } + @Override + public @Nullable LookupElementRenderer getExpensiveRenderer() { + if(item.getDetail() == null && supportResolveCompletion) { + // The LSP completion item 'detail' is not filled, try to resolve it + // inside getExpensiveRenderer() which should not impact performance. + CompletionItem resolved = getResolvedCompletionItem(); + if (resolved != null && resolved.getDetail() != null) { + item.setDetail(resolved.getDetail()); + + return new LookupElementRenderer() { + @Override + public void renderElement(LookupElement element, LookupElementPresentation presentation) { + LSPCompletionProposal.this.renderElement(presentation); + } + }; + } + } + return null; + } protected void apply(Document document, char trigger, int stateMask, int offset) { String insertText = null; Either eitherTextEdit = item.getTextEdit(); @@ -316,6 +338,13 @@ protected void apply(Document document, char trigger, int stateMask, int offset) } List additionalEdits = item.getAdditionalTextEdits(); + if (additionalEdits == null && supportResolveCompletion) { + // The LSP completion item 'additionalEdits' is not filled, try to resolve it. + CompletionItem resolved = getResolvedCompletionItem(); + if (resolved != null) { + additionalEdits = resolved.getAdditionalTextEdits(); + } + } if (additionalEdits != null && !additionalEdits.isEmpty()) { List allEdits = new ArrayList<>(); allEdits.add(textEdit); @@ -341,12 +370,8 @@ protected void apply(Document document, char trigger, int stateMask, int offset) private void executeCustomCommand(@NotNull Command command, URI documentUri) { Project project = editor.getProject(); // Execute custom command of the completion item. - LanguageServiceAccessor.getInstance(project) - .resolveServerDefinition(languageServer.getServer()).map(definition -> definition.getId()) - .ifPresent(id -> { - CommandExecutor.executeCommand(command, documentUri, project, id); - }); - + String languageServerId = languageServer.getServerWrapper().getServerDefinition().getId(); + CommandExecutor.executeCommand(command, documentUri, project, languageServerId); } public @Nullable Range getTextEditRange() { @@ -421,23 +446,11 @@ public CompletionItem getItem() { public MarkupContent getDocumentation() { if (item.getDocumentation() == null && supportResolveCompletion) { - try { - CompletionItem resolved = languageServer.getServer() - .getTextDocumentService() - .resolveCompletionItem(item) - .get(1000, TimeUnit.MILLISECONDS); - if (resolved != null) { - item.setDocumentation(resolved.getDocumentation()); - } - } catch (ExecutionException e) { - if (!(e.getCause() instanceof CancellationException)) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - } catch (TimeoutException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } catch (InterruptedException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - Thread.currentThread().interrupt(); + // The LSP completion item 'documentation' is not filled, try to resolve it + // As documentation is computed in a Thread it should not impact performance. + CompletionItem resolved = getResolvedCompletionItem(); + if (resolved != null) { + item.setDocumentation(resolved.getDocumentation()); } } return getDocumentation(item.getDocumentation()); @@ -453,4 +466,32 @@ private static MarkupContent getDocumentation(Either docu } return documentation.getRight(); } + + /** + * Returns the resolved completion item and null otherwise. + * + * @return the resolved completion item and null otherwise. + */ + private CompletionItem getResolvedCompletionItem() { + if (resolvedCompletionItemFuture == null) { + resolvedCompletionItemFuture = languageServer.getServer() + .getTextDocumentService() + .resolveCompletionItem(item); + } + try { + // Wait upon the future is finished and stop the wait if there are some ProcessCanceledException. + waitUntilDone(resolvedCompletionItemFuture, file); + } catch (CancellationException | ProcessCanceledException e) { + return null; + } catch (ExecutionException e) { + LOGGER.error("Error while consuming LSP 'completionItem/resolve' request", e); + return null; + } + + ProgressManager.checkCanceled(); + if (isDoneNormally(resolvedCompletionItemFuture)) { + return resolvedCompletionItemFuture.getNow(null); + } + return null; + } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionSupport.java new file mode 100644 index 00000000..76466f9f --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/completion/LSPCompletionSupport.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2024 Red Hat, Inc. + * Distributed under license by Red Hat, Inc. All rights reserved. + * This program is made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v20.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.features.completion; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPRequestConstants; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.features.AbstractLSPFeatureSupport; +import com.redhat.devtools.lsp4ij.internal.CancellationSupport; +import com.redhat.devtools.lsp4ij.internal.CompletableFutures; +import org.eclipse.lsp4j.CompletionParams; +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * LSP Completion support which loads and caches Completion information by consuming: + * + *
    + *
  • LSP 'textDocument/completion' requests
  • + *
+ */ +public class LSPCompletionSupport extends AbstractLSPFeatureSupport> { + + private Integer previousOffset; + public LSPCompletionSupport(@NotNull PsiFile file) { + super(file); + } + + public CompletableFuture> getCompletions(LSPCompletionParams params) { + int offset = params.getOffset(); + if (previousOffset != null && !previousOffset.equals(offset)) { + super.cancel(); + } + previousOffset = offset; + return super.getFeatureData(params); + } + + @Override + protected CompletableFuture> doLoad(@NotNull LSPCompletionParams params, + @NotNull CancellationSupport cancellationSupport) { + PsiFile file = super.getFile(); + return getCompletions(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + } + + private static @NotNull CompletableFuture> getCompletions(@NotNull VirtualFile file, + @NotNull Project project, + @NotNull LSPCompletionParams params, + @NotNull CancellationSupport cancellationSupport) { + + return LanguageServiceAccessor.getInstance(project) + .getLanguageServers(file, LanguageServerItem::isCompletionSupported) + .thenComposeAsync(languageServers -> { + // Here languageServers is the list of language servers which matches the given file + // and which have completion capability + if (languageServers.isEmpty()) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + + // Collect list of textDocument/completion future for each language servers + List>> completionPerServerFutures = languageServers + .stream() + .map(languageServer -> getCompletionsFor(params, languageServer, cancellationSupport)) + .toList(); + + // Merge list of textDocument/completion future in one future which return the list of completion items + return CompletableFutures.mergeInOneFuture(completionPerServerFutures, cancellationSupport); + }); + } + + private static CompletableFuture> getCompletionsFor(@NotNull CompletionParams params, + @NotNull LanguageServerItem languageServer, + @NotNull CancellationSupport cancellationSupport) { + return cancellationSupport.execute(languageServer + .getTextDocumentService() + .completion(params), languageServer, LSPRequestConstants.TEXT_DOCUMENT_DOCUMENT_COMPLETION) + .thenApplyAsync(result -> { + if (result == null) { + // textDocument/completion may return null + return Collections.emptyList(); + } + return List.of(new CompletionData(result, languageServer)); + }); + } + + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/internal/SupportedFeatures.java b/src/main/java/com/redhat/devtools/lsp4ij/internal/SupportedFeatures.java index 87687f4b..116118d9 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/internal/SupportedFeatures.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/internal/SupportedFeatures.java @@ -58,7 +58,11 @@ public class SupportedFeatures { .setDocumentationFormat(List.of(MarkupKind.MARKDOWN, MarkupKind.PLAINTEXT)); completionItemCapabilities.setInsertTextModeSupport(new CompletionItemInsertTextModeSupportCapabilities(List.of(InsertTextMode.AsIs, InsertTextMode.AdjustIndentation))); - completionItemCapabilities.setResolveSupport(new CompletionItemResolveSupportCapabilities(List.of("documentation" /*, "detail", "additionalTextEdits" */))); + completionItemCapabilities.setResolveSupport(new CompletionItemResolveSupportCapabilities( + List.of( + "documentation" , + "detail", + "additionalTextEdits" ))); CompletionCapabilities completionCapabilities = new CompletionCapabilities(completionItemCapabilities); completionCapabilities.setCompletionList(new CompletionListCapabilities(List.of("editRange"))); textDocumentClientCapabilities.setCompletion(completionCapabilities);