diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServerItem.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServerItem.java new file mode 100644 index 000000000..d179b0a39 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServerItem.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2023 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.intellij.lsp4ij; + +import org.eclipse.lsp4j.services.LanguageServer; + +/** + * Item which stores the initialized LSP4j language server and the language server wrapper. + */ +public class LanguageServerItem { + + private final LanguageServerWrapper serverWrapper; + private final LanguageServer server; + + public LanguageServerItem(LanguageServer server, LanguageServerWrapper serverWrapper) { + this.server = server; + this.serverWrapper = serverWrapper; + } + + /** + * Returns the LSP4j language server. + * + * @return the LSP4j language server. + */ + public LanguageServer getServer() { + return server; + } + + /** + * Returns the language server wrapper. + * + * @return the language server wrapper. + */ + public LanguageServerWrapper getServerWrapper() { + return serverWrapper; + } + +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java index b06bb47c4..53c3981c2 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/LanguageServiceAccessor.java @@ -11,18 +11,14 @@ package com.redhat.devtools.intellij.lsp4ij; import com.intellij.lang.Language; -import com.intellij.lang.LanguageUtil; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; import com.intellij.openapi.module.Module; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.vfs.VirtualFile; import com.redhat.devtools.intellij.lsp4ij.server.StreamConnectionProvider; import org.eclipse.lsp4j.ServerCapabilities; -import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.services.LanguageServer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +29,6 @@ import java.net.URI; import java.util.*; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -56,14 +51,37 @@ private LanguageServiceAccessor(Project project) { private final Set startedServers = new HashSet<>(); private Map providersToLSDefinitions = new HashMap<>(); - /** - * This is meant for test code to clear state that might have leaked from other - * tests. It isn't meant to be used in production code. - */ - public void clearStartedServers() { - synchronized (startedServers) { - startedServers.forEach(LanguageServerWrapper::stop); - startedServers.clear(); + @Nonnull + public CompletableFuture> getLanguageServers(@Nonnull Document document, + Predicate filter) { + URI uri = LSPIJUtils.toUri(document); + if (uri == null) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + final List servers = Collections.synchronizedList(new ArrayList<>()); + try { + return CompletableFuture.allOf(getLSWrappers(document).stream().map(wrapper -> + wrapper.getInitializedServer() + .thenComposeAsync(server -> { + if (server != null && wrapper.isEnabled() && (filter == null || filter.test(wrapper.getServerCapabilities()))) { + try { + return wrapper.connect(document); + } catch (IOException ex) { + LOGGER.warn(ex.getLocalizedMessage(), ex); + } + } + return CompletableFuture.completedFuture(null); + }).thenAccept(server -> { + if (server != null) { + servers.add(new LanguageServerItem(server, wrapper)); + } + })).toArray(CompletableFuture[]::new)) + .thenApply(theVoid -> servers); + } catch (final ProcessCanceledException cancellation) { + throw cancellation; + } catch (final Exception e) { + LOGGER.warn(e.getLocalizedMessage(), e); + return CompletableFuture.completedFuture(Collections.emptyList()); } } @@ -76,10 +94,6 @@ public Set getStartedServers() { return startedServers; } - void shutdownAllDispatchers() { - startedServers.forEach(LanguageServerWrapper::stopDispatcher); - } - public void projectClosing(Project project) { // On project closing, we dispose all language servers startedServers.forEach(ls -> { @@ -89,173 +103,6 @@ public void projectClosing(Project project) { }); } - /** - * A bean storing association of a Document/File with a language server. - */ - public static class LSPDocumentInfo { - - private final @Nonnull - URI fileUri; - private final @Nonnull - Document document; - private final @Nonnull - LanguageServerWrapper wrapper; - - private LSPDocumentInfo(@Nonnull URI fileUri, @Nonnull Document document, - @Nonnull LanguageServerWrapper wrapper) { - this.fileUri = fileUri; - this.document = document; - this.wrapper = wrapper; - } - - public @Nonnull - Document getDocument() { - return this.document; - } - - /** - * TODO consider directly returning a {@link TextDocumentIdentifier} - * - * @return - */ - public @Nonnull - URI getFileUri() { - return this.fileUri; - } - - /** - * Returns the language server, regardless of if it is initialized. - * - * @deprecated use {@link #getInitializedLanguageClient()} instead. - */ - @Deprecated - public LanguageServer getLanguageClient() { - try { - return this.wrapper.getInitializedServer().get(); - } catch (ExecutionException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - return this.wrapper.getServer(); - } catch (InterruptedException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - Thread.currentThread().interrupt(); - return this.wrapper.getServer(); - } - } - - public int getVersion() { - return wrapper.getVersion(LSPIJUtils.getFile(document)); - } - - public CompletableFuture getInitializedLanguageClient() { - return this.wrapper.getInitializedServer(); - } - - public @Nullable - ServerCapabilities getCapabilites() { - return this.wrapper.getServerCapabilities(); - } - - public boolean isActive() { - return this.wrapper.isActive(); - } - } - - - public @Nonnull - List> getInitializedLanguageServers(@Nonnull VirtualFile file, - @Nullable Predicate request) throws IOException { - synchronized (startedServers) { - Collection wrappers = getLSWrappers(file, request); - return wrappers.stream().map(wrapper -> wrapper.getInitializedServer().thenApplyAsync(server -> { - try { - wrapper.connect(file, null); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - return server; - })).collect(Collectors.toList()); - } - } - - public void disableLanguageServerContentType( - @Nonnull ContentTypeToLanguageServerDefinition contentTypeToLSDefinition) { - Optional result = startedServers.stream() - .filter(server -> server.serverDefinition.equals(contentTypeToLSDefinition.getValue())).findFirst(); - if (result.isPresent()) { - Language language = contentTypeToLSDefinition.getKey(); - if (language != null) { - result.get().disconnectContentType(language); - } - } - - } - - public void enableLanguageServerContentType( - @Nonnull ContentTypeToLanguageServerDefinition contentTypeToLSDefinition, - @Nonnull Editor[] editors) { - for (Editor editor : editors) { - VirtualFile editorFile = LSPIJUtils.getFile(editor.getDocument()); - Language language = contentTypeToLSDefinition.getKey(); - LanguageServersRegistry.LanguageServerDefinition lsDefinition = contentTypeToLSDefinition.getValue(); - Language contentLanguage = LanguageUtil.getLanguageForPsi(project, editorFile); - if (contentTypeToLSDefinition.isEnabled() && language != null && contentLanguage != null - && contentLanguage.isKindOf(language) - && lsDefinition != null) { - try { - getInitializedLanguageServer(editorFile, lsDefinition, capabilities -> true); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - } - } - - } - - /** - * Get the requested language server instance for the given file. Starts the language server if not already started. - * - * @param file - * @param lsDefinition - * @param capabilitiesPredicate a predicate to check capabilities - * @return a LanguageServer for the given file, which is defined with provided server ID and conforms to specified request - * @deprecated use {@link #getInitializedLanguageServer(IFile, LanguageServerDefinition, Predicate)} instead. - */ - @Deprecated - public LanguageServer getLanguageServer(@Nonnull VirtualFile file, @Nonnull LanguageServersRegistry.LanguageServerDefinition lsDefinition, - Predicate capabilitiesPredicate) - throws IOException { - LanguageServerWrapper wrapper = getLSWrapperForConnection(LSPIJUtils.getProject(file), lsDefinition, LSPIJUtils.toUri(file)); - if (capabilitiesPredicate == null - || wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */ - || capabilitiesPredicate.test(wrapper.getServerCapabilities())) { - wrapper.connect(file, null); - return wrapper.getServer(); - } - return null; - } - - /** - * Get the requested language server instance for the given file. Starts the language server if not already started. - * - * @param file - * @param lsDefinition - * @param capabilitiesPredicate a predicate to check capabilities - * @return a LanguageServer for the given file, which is defined with provided server ID and conforms to specified request - */ - public CompletableFuture getInitializedLanguageServer(@Nonnull VirtualFile file, - @Nonnull LanguageServersRegistry.LanguageServerDefinition lsDefinition, - Predicate capabilitiesPredicate) - throws IOException { - LanguageServerWrapper wrapper = getLSWrapperForConnection(LSPIJUtils.getProject(file), lsDefinition, LSPIJUtils.toUri(file)); - if (capabilitiesPredicate == null - || wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */ - || capabilitiesPredicate.test(wrapper.getServerCapabilities())) { - wrapper.connect(file, null); - return wrapper.getInitializedServer(); - } - return null; - } - /** * Get the requested language server instance for the given document. Starts the * language server if not already started. @@ -299,53 +146,6 @@ private static boolean capabilitiesComply(LanguageServerWrapper wrapper, || capabilitiesPredicate.test(wrapper.getServerCapabilities()); } - - /** - * TODO we need a similar method for generic IDocument (enabling non-IFiles) - * - * @param file - * @param request - * @return - * @throws IOException - * @noreference This method is currently internal and should only be referenced - * for testing - */ - @Nonnull - public Collection getLSWrappers(@Nonnull VirtualFile file, - @Nullable Predicate request) throws IOException { - LinkedHashSet res = new LinkedHashSet<>(); - Module project = LSPIJUtils.getProject(file); - if (project == null) { - return res; - } - - res.addAll(getMatchingStartedWrappers(file, request)); - - // look for running language servers via content-type - Queue contentTypes = new LinkedList<>(); - Set addedContentTypes = new HashSet<>(); - contentTypes.add(LSPIJUtils.getFileLanguage(file, project.getProject())); - addedContentTypes.addAll(contentTypes); - - while (!contentTypes.isEmpty()) { - Language contentType = contentTypes.poll(); - if (contentType == null) { - continue; - } - for (ContentTypeToLanguageServerDefinition mapping : LanguageServersRegistry.getInstance().findProviderFor(contentType)) { - if (mapping != null && mapping.getValue() != null && mapping.isEnabled()) { - LanguageServerWrapper wrapper = getLSWrapperForConnection(project, mapping.getValue(), LSPIJUtils.toUri(file)); - if (request == null - || wrapper.getServerCapabilities() == null /* null check is workaround for https://github.com/TypeFox/ls-api/issues/47 */ - || request.test(wrapper.getServerCapabilities())) { - res.add(wrapper); - } - } - } - } - return res; - } - @Nonnull private Collection getLSWrappers(@Nonnull Document document) { LinkedHashSet res = new LinkedHashSet<>(); @@ -409,52 +209,6 @@ private Collection getLSWrappers(@Nonnull Document docume } } - /** - * Return existing {@link LanguageServerWrapper} for the given connection. If - * not found, create a new one with the given connection and register it for - * this project/content-type. - * - * @param project - * @param serverDefinition - * @return - * @throws IOException - * @Deprecated will be made private soon - * @noreference will be made private soon - * @deprecated - */ - @Deprecated - public LanguageServerWrapper getLSWrapperForConnection(@Nonnull Module project, - @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition) throws IOException { - return getLSWrapperForConnection(project, serverDefinition, null); - } - - @Deprecated - private LanguageServerWrapper getLSWrapperForConnection(@Nonnull Module project, - @Nonnull LanguageServersRegistry.LanguageServerDefinition serverDefinition, @Nullable URI initialPath) throws IOException { - if (!serverDefinition.isEnabled()) { - // don't return a language server wrapper for the given server definition - return null; - } - LanguageServerWrapper wrapper = null; - - synchronized (startedServers) { - for (LanguageServerWrapper startedWrapper : getStartedLSWrappers(project)) { - if (startedWrapper.serverDefinition.equals(serverDefinition)) { - wrapper = startedWrapper; - break; - } - } - if (wrapper == null) { - wrapper = project != null ? new LanguageServerWrapper(project, serverDefinition) : - new LanguageServerWrapper(serverDefinition, initialPath); - wrapper.start(); - } - - startedServers.add(wrapper); - } - return wrapper; - } - private LanguageServerWrapper getLSWrapperForConnection(Document document, LanguageServersRegistry.LanguageServerDefinition serverDefinition, URI initialPath) throws IOException { if (!serverDefinition.isEnabled()) { @@ -523,19 +277,6 @@ public List getActiveLanguageServers(Predicate getLanguageServers(@Nonnull Module project, - Predicate request) { - return getLanguageServers(project, request, false); - } - /** * Gets list of LS initialized for given project * @@ -565,63 +306,6 @@ public List getLanguageServers(@Nullable Module project, return serverInfos; } - protected LanguageServersRegistry.LanguageServerDefinition getLSDefinition(@Nonnull StreamConnectionProvider provider) { - return providersToLSDefinitions.get(provider); - } - - @Nonnull - public List getLSPDocumentInfosFor(@Nonnull Document document, @Nonnull Predicate capabilityRequest) { - URI fileUri = LSPIJUtils.toUri(document); - List res = new ArrayList<>(); - try { - getLSWrappers(document).stream().filter(wrapper -> wrapper.getServerCapabilities() == null - || capabilityRequest.test(wrapper.getServerCapabilities())).forEach(wrapper -> { - try { - wrapper.connect(document); - } catch (IOException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - res.add(new LSPDocumentInfo(fileUri, document, wrapper)); - }); - } catch (final Exception e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - return res; - } - - @Nonnull - public CompletableFuture>> getLanguageServers(@Nonnull Document document, - Predicate filter) { - URI uri = LSPIJUtils.toUri(document); - if (uri == null) { - return CompletableFuture.completedFuture(Collections.emptyList()); - } - final List> res = Collections.synchronizedList(new ArrayList<>()); - try { - return CompletableFuture.allOf(getLSWrappers(document).stream().map(wrapper -> - wrapper.getInitializedServer() - .thenComposeAsync(server -> { - if (server != null && wrapper.isEnabled() && (filter == null || filter.test(wrapper.getServerCapabilities()))) { - try { - return wrapper.connect(document); - } catch (IOException ex) { - LOGGER.warn(ex.getLocalizedMessage(), ex); - } - } - return CompletableFuture.completedFuture(null); - }).thenAccept(server -> { - if (server != null) { - res.add(new Pair(wrapper, server)); - } - })).toArray(CompletableFuture[]::new)).thenApply(theVoid -> res); - } catch (final ProcessCanceledException cancellation) { - throw cancellation; - } catch (final Exception e) { - LOGGER.warn(e.getLocalizedMessage(), e); - } - return CompletableFuture.completedFuture(Collections.emptyList()); - } - public boolean checkCapability(LanguageServer languageServer, Predicate condition) { return startedServers.stream().filter(wrapper -> wrapper.isActive() && wrapper.getServer() == languageServer) .anyMatch(wrapper -> condition.test(wrapper.getServerCapabilities())); diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/LSPCompletableFuture.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/LSPCompletableFuture.java index 1e0861c69..9b7141d3a 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/LSPCompletableFuture.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/client/LSPCompletableFuture.java @@ -144,7 +144,9 @@ private CancellablePromise> nonBlockingReadActionPromise(boolea public boolean cancel(boolean mayInterruptIfRunning) { if (nonBlockingReadActionPromise != null) { // cancel the current promise - nonBlockingReadActionPromise.cancel(mayInterruptIfRunning); + if (!nonBlockingReadActionPromise.isDone()) { + nonBlockingReadActionPromise.cancel(mayInterruptIfRunning); + } } return super.cancel(mayInterruptIfRunning); } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/CancellationSupport.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/CancellationSupport.java new file mode 100644 index 000000000..3d8c28073 --- /dev/null +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/internal/CancellationSupport.java @@ -0,0 +1,73 @@ +/******************************************************************************* + * Copyright (c) 2023 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.intellij.lsp4ij.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 https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#cancelRequest + */ +public class CancellationSupport implements CancelChecker { + + private final List> futuresToCancel; + + private boolean cancelled; + + public CancellationSupport() { + this.futuresToCancel = new ArrayList>(); + this.cancelled = false; + } + + public CompletableFuture execute(CompletableFuture future) { + this.futuresToCancel.add(future); + return future; + } + + /** + * Cancel all LSP requests. + */ + public void cancel() { + if (cancelled == true) { + return; + } + this.cancelled = true; + for (CompletableFuture futureToCancel : futuresToCancel) { + if (!futureToCancel.isDone()) { + 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 IJ LookupElement fo). + // 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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/codelens/LSPCodelensInlayProvider.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/codelens/LSPCodelensInlayProvider.java index 421c5ae36..f8dbb5d2f 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/codelens/LSPCodelensInlayProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/codelens/LSPCodelensInlayProvider.java @@ -19,6 +19,7 @@ import com.intellij.codeInsight.hints.presentation.SequencePresentation; 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; @@ -27,6 +28,7 @@ import com.redhat.devtools.intellij.lsp4ij.AbstractLSPInlayProvider; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.CodeLensParams; import org.eclipse.lsp4j.Command; @@ -64,29 +66,36 @@ public InlayHintsCollector getCollectorFor(@NotNull PsiFile psiFile, return new FactoryInlayHintsCollector(editor) { @Override public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink) { + Project project = psiElement.getProject(); + if (project.isDisposed()) { + // The project has been closed, don't collect code lenses. + return false; + } + Document document = editor.getDocument(); + final CancellationSupport cancellationSupport = new CancellationSupport(); try { - Document document = editor.getDocument(); - Project project = psiElement.getProject(); - if (project.isDisposed()) { - // The project has been closed, don't collect code lenses. - return false; - } URI docURI = LSPIJUtils.toUri(document); if (docURI != null) { CodeLensParams param = new CodeLensParams(new TextDocumentIdentifier(docURI.toString())); BlockingDeque> pairs = new LinkedBlockingDeque<>(); - CompletableFuture future = collect(document, project, param, pairs); - List>> codeLenses = createCodeLenses(document, pairs, future); - Map>>> elements = codeLenses.stream().collect(Collectors.groupingBy(p -> p.first)); - elements.forEach((offset, list) -> - inlayHintsSink.addBlockElement( - offset, - true, - true, - 0, - toPresentation(editor, offset, list, getFactory())) - ); + + CompletableFuture future = collect(document, project, param, pairs, cancellationSupport); + List>> codeLenses = createCodeLenses(document, pairs, future, cancellationSupport); + codeLenses.stream() + .collect(Collectors.groupingBy(p -> p.first)) + .forEach((offset, list) -> + inlayHintsSink.addBlockElement( + offset, + true, + true, + 0, + toPresentation(editor, offset, list, getFactory())) + ); } + } catch (ProcessCanceledException e) { + // Cancel all LSP requests + cancellationSupport.cancel(); + throw e; } catch (InterruptedException e) { LOGGER.warn(e.getLocalizedMessage(), e); Thread.currentThread().interrupt(); @@ -95,38 +104,45 @@ public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @ } @NotNull - private List>> createCodeLenses(Document document, BlockingDeque> pairs, CompletableFuture future) throws InterruptedException { + private List>> createCodeLenses(Document document, BlockingDeque> pairs, CompletableFuture future, CancellationSupport cancellationSupport) throws InterruptedException { List>> codelenses = new ArrayList<>(); while (!future.isDone() || !pairs.isEmpty()) { - ProgressManager.checkCanceled(); - Pair pair = pairs.poll(25, TimeUnit.MILLISECONDS); - if (pair != null) { - int offset = LSPIJUtils.toOffset(pair.getFirst().getRange().getStart(), document); - codelenses.add(Pair.create(offset, pair)); + try { + ProgressManager.checkCanceled(); + Pair pair = pairs.poll(25, TimeUnit.MILLISECONDS); + if (pair != null) { + int offset = LSPIJUtils.toOffset(pair.getFirst().getRange().getStart(), document); + codelenses.add(Pair.create(offset, pair)); + } + } catch (ProcessCanceledException e) { + cancellationSupport.cancel(); + throw e; } } return codelenses; } - private CompletableFuture collect(Document document, Project project, CodeLensParams param, BlockingDeque> pairs) { + private CompletableFuture collect(Document document, Project project, CodeLensParams param, BlockingDeque> pairs, CancellationSupport cancellationSupport) { return LanguageServiceAccessor.getInstance(project) .getLanguageServers(document, capabilities -> capabilities.getCodeLensProvider() != null) - .thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream() - .map(languageServer -> languageServer.getSecond().getTextDocumentService().codeLens(param) - .thenAcceptAsync(codeLenses -> { - // textDocument/codeLens may return null - if (codeLenses != null) { - codeLenses.stream() - .filter(Objects::nonNull) - .forEach(codeLens -> { - if (getCodeLensContent(codeLens) != null) { - // The codelens content is filled, display it - pairs.add(new Pair(codeLens, languageServer.getSecond())); + .thenComposeAsync(languageServers -> + cancellationSupport.execute(CompletableFuture.allOf(languageServers.stream() + .map(languageServer -> + cancellationSupport.execute(languageServer.getServer().getTextDocumentService().codeLens(param)) + .thenAcceptAsync(codeLenses -> { + // textDocument/codeLens may return null + if (codeLenses != null) { + codeLenses.stream() + .filter(Objects::nonNull) + .forEach(codeLens -> { + if (getCodeLensContent(codeLens) != null) { + // The codelens content is filled, display it + pairs.add(new Pair(codeLens, languageServer.getServer())); + } + }); } - }); - } - })) - .toArray(CompletableFuture[]::new))); + })) + .toArray(CompletableFuture[]::new)))); } }; } @@ -151,7 +167,7 @@ private InlayPresentation toPresentation( } else { // Codelens defines a Command, create a clickable inlay hint InlayPresentation clickableText = factory.referenceOnHover(text, (event, translated) -> - executeClientCommand(p.second.second, p.second.first, (Component) event.getSource(), editor.getProject()) + executeClientCommand(p.second.second, p.second.first, (Component) event.getSource(), editor.getProject()) ); presentations.add(clickableText); } @@ -162,10 +178,10 @@ private InlayPresentation toPresentation( private void executeClientCommand(LanguageServer languageServer, CodeLens codeLens, Component source, Project project) { if (LanguageServiceAccessor.getInstance(project).checkCapability(languageServer, capabilities -> - Boolean.TRUE.equals(capabilities.getCodeLensProvider().getResolveProvider())) + Boolean.TRUE.equals(capabilities.getCodeLensProvider().getResolveProvider())) ) { languageServer.getTextDocumentService().resolveCodeLens(codeLens).thenAcceptAsync(resolvedCodeLens -> - executeClientCommand(source, resolvedCodeLens.getCommand()) + executeClientCommand(source, resolvedCodeLens.getCommand()) ); } else { executeClientCommand(source, codeLens.getCommand()); diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSPCompletionContributor.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSPCompletionContributor.java index a9ff3ac97..93011bd23 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSPCompletionContributor.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/completion/LSPCompletionContributor.java @@ -20,13 +20,13 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; -import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Pair; import com.intellij.psi.PsiFile; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; -import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper; +import com.redhat.devtools.intellij.lsp4ij.LanguageServerItem; import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; import org.apache.commons.lang.StringUtils; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; @@ -54,8 +54,15 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No PsiFile file = parameters.getOriginalFile(); Project project = file.getProject(); int offset = parameters.getOffset(); - CompletableFuture>> completionLanguageServersFuture = initiateLanguageServers(project, document); + + ProgressManager.checkCanceled(); + + final CancellationSupport cancellationSupport = new CancellationSupport(); try { + CompletableFuture> completionLanguageServersFuture = initiateLanguageServers(project, document); + 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 @@ -63,11 +70,16 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No */ CompletionParams params = LSPIJUtils.toCompletionParams(LSPIJUtils.toUri(document), offset, document); BlockingDeque, CompletionList>, LanguageServer>> proposals = new LinkedBlockingDeque<>(); + CompletableFuture future = completionLanguageServersFuture - .thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream() - .map(languageServer -> languageServer.getSecond().getTextDocumentService().completion(params) - .thenAcceptAsync(completion -> proposals.add(new Pair<>(completion, languageServer.getSecond())))) - .toArray(CompletableFuture[]::new))); + .thenComposeAsync(languageServers -> cancellationSupport.execute( + CompletableFuture.allOf(languageServers.stream() + .map(languageServer -> + cancellationSupport.execute(languageServer.getServer().getTextDocumentService().completion(params)) + .thenAcceptAsync(completion -> proposals.add(new Pair<>(completion, languageServer.getServer())))) + .toArray(CompletableFuture[]::new)))); + + ProgressManager.checkCanceled(); while (!future.isDone() || !proposals.isEmpty()) { ProgressManager.checkCanceled(); Pair, CompletionList>, LanguageServer> pair = proposals.poll(25, TimeUnit.MILLISECONDS); @@ -75,11 +87,12 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No Either, CompletionList> completion = pair.getFirst(); if (completion != null) { CompletionPrefix completionPrefix = new CompletionPrefix(offset, document); - addCompletionItems(file, editor, completionPrefix, pair.getFirst(), pair.getSecond(), result); + addCompletionItems(file, editor, completionPrefix, pair.getFirst(), pair.getSecond(), result, cancellationSupport); } } } } catch (ProcessCanceledException cancellation) { + cancellationSupport.cancel(); throw cancellation; } catch (RuntimeException | InterruptedException e) { LOGGER.warn(e.getLocalizedMessage(), e); @@ -88,7 +101,7 @@ public void fillCompletionVariants(@NotNull CompletionParameters parameters, @No } private void addCompletionItems(PsiFile file, Editor editor, CompletionPrefix completionPrefix, Either, - CompletionList> completion, LanguageServer languageServer, @NotNull CompletionResultSet result) { + CompletionList> completion, LanguageServer languageServer, @NotNull CompletionResultSet result, CancellationSupport cancellationSupport) { CompletionItemDefaults itemDefaults = null; List items = null; if (completion.isLeft()) { @@ -103,6 +116,7 @@ private void addCompletionItems(PsiFile file, Editor editor, CompletionPrefix co // Invalid completion Item, ignore it continue; } + cancellationSupport.checkCanceled(); // Create lookup item var lookupItem = createLookupItem(file, editor, completionPrefix.getCompletionOffset(), item, itemDefaults, languageServer); // Group it by using completion item kind @@ -112,10 +126,10 @@ private void addCompletionItems(PsiFile file, Editor editor, CompletionPrefix co if (prefix != null) { // Add the IJ completion item (lookup item) by using the computed prefix result.withPrefixMatcher(prefix) - .caseInsensitive() // set case insensitive to search Java class which starts with upper case + .caseInsensitive() // set case-insensitive to search Java class which starts with upper case .addElement(groupedLookupItem); } else { - // Should happens rarely, only when text edit is for multi-lines or if completion is triggered outside the text edit range. + // Should happen rarely, only when text edit is for multi-lines or if completion is triggered outside the text edit range. // Add the IJ completion item (lookup item) which will use the IJ prefix result.addElement(groupedLookupItem); } @@ -155,7 +169,7 @@ private static LookupElement createErrorProposal(int offset, Exception ex) { return LookupElementBuilder.create("Error while computing completion", ""); } - private static CompletableFuture>> initiateLanguageServers(Project project, Document document) { + private static CompletableFuture> initiateLanguageServers(Project project, Document document) { return LanguageServiceAccessor.getInstance(project).getLanguageServers(document, capabilities -> { CompletionOptions provider = capabilities.getCompletionProvider(); diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/documentLink/LSPDocumentLinkAnnotator.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/documentLink/LSPDocumentLinkAnnotator.java index f614b7bd1..e7f40db9a 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/documentLink/LSPDocumentLinkAnnotator.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/documentLink/LSPDocumentLinkAnnotator.java @@ -24,12 +24,8 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiFile; -import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; -import com.redhat.devtools.intellij.lsp4ij.LSPVirtualFileWrapper; -import com.redhat.devtools.intellij.lsp4ij.LanguageServerWrapper; -import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; -import com.redhat.devtools.intellij.lsp4ij.operations.highlight.LSPHighlightPsiElement; -import org.eclipse.lsp4j.DocumentHighlight; +import com.redhat.devtools.intellij.lsp4ij.*; +import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; import org.eclipse.lsp4j.DocumentLink; import org.eclipse.lsp4j.DocumentLinkParams; import org.jetbrains.annotations.NotNull; @@ -40,7 +36,6 @@ import java.net.URI; import java.util.List; import java.util.concurrent.*; -import java.util.logging.Level; /** * Intellij {@link ExternalAnnotator} implementation which collect LSP document links and display them with underline style. @@ -56,6 +51,7 @@ public LSPVirtualFileWrapper collectInformation(@NotNull PsiFile file, @NotNull if (uri == null) { return null; } + final CancellationSupport cancellationSupport = new CancellationSupport(); try { ProgressManager.checkCanceled(); DocumentLinkParams params = new DocumentLinkParams(LSPIJUtils.toTextDocumentIdentifier(uri)); @@ -63,15 +59,17 @@ public LSPVirtualFileWrapper collectInformation(@NotNull PsiFile file, @NotNull BlockingDeque, LanguageServerWrapper>> documentLinks = new LinkedBlockingDeque<>(); CompletableFuture future = LanguageServiceAccessor.getInstance(editor.getProject()).getLanguageServers(document, - capabilities -> capabilities.getDocumentLinkProvider() != null) + capabilities -> capabilities.getDocumentLinkProvider() != null) .thenAcceptAsync(languageServers -> - CompletableFuture.allOf(languageServers.stream() - .map(languageServer -> Pair.pair(languageServer.getSecond().getTextDocumentService().documentLink(params), languageServer.getFirst())) + cancellationSupport.execute(CompletableFuture.allOf(languageServers.stream() + .map(languageServer -> Pair.pair( + cancellationSupport.execute(languageServer.getServer().getTextDocumentService().documentLink(params)) + , languageServer.getServerWrapper())) .map(request -> request.getFirst().thenAcceptAsync(result -> { if (result != null) { documentLinks.add(Pair.pair(result, request.getSecond())); } - })).toArray(CompletableFuture[]::new))); + })).toArray(CompletableFuture[]::new)))); while (!future.isDone() || !documentLinks.isEmpty()) { ProgressManager.checkCanceled(); Pair, LanguageServerWrapper> links = documentLinks.poll(25, TimeUnit.MILLISECONDS); @@ -80,8 +78,8 @@ public LSPVirtualFileWrapper collectInformation(@NotNull PsiFile file, @NotNull .updateDocumentLink(links.getFirst(), links.getSecond()); } } - } catch (ProcessCanceledException cancellation) { + cancellationSupport.cancel(); throw cancellation; } catch (InterruptedException e) { LOGGER.warn(e.getLocalizedMessage(), e); diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/highlight/LSPHighlightUsagesHandlerFactory.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/highlight/LSPHighlightUsagesHandlerFactory.java index ad04201d6..be2d6f00d 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/highlight/LSPHighlightUsagesHandlerFactory.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/highlight/LSPHighlightUsagesHandlerFactory.java @@ -18,10 +18,10 @@ import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; import org.eclipse.lsp4j.DocumentHighlight; import org.eclipse.lsp4j.DocumentHighlightParams; import org.eclipse.lsp4j.Position; @@ -46,45 +46,48 @@ public class LSPHighlightUsagesHandlerFactory implements HighlightUsagesHandlerF @Override public @Nullable HighlightUsagesHandlerBase createHighlightUsagesHandler(@NotNull Editor editor, @NotNull PsiFile file) { List targets = getTargets(editor, file); - return targets.isEmpty()?null:new LSPHighlightUsagesHandler(editor, file, targets); + return targets.isEmpty() ? null : new LSPHighlightUsagesHandler(editor, file, targets); } private List getTargets(Editor editor, PsiFile file) { List elements = new ArrayList<>(); + final CancellationSupport cancellationSupport = new CancellationSupport(); try { int offset = TargetElementUtil.adjustOffset(file, editor.getDocument(), editor.getCaretModel().getOffset()); Document document = editor.getDocument(); Position position; position = LSPIJUtils.toPosition(offset, document); URI uri = LSPIJUtils.toUri(document); - if(uri == null) { + if (uri == null) { return Collections.emptyList(); } ProgressManager.checkCanceled(); TextDocumentIdentifier identifier = new TextDocumentIdentifier(uri.toString()); DocumentHighlightParams params = new DocumentHighlightParams(identifier, position); BlockingDeque highlights = new LinkedBlockingDeque<>(); - CompletableFuture future = LanguageServiceAccessor.getInstance(editor.getProject()).getLanguageServers(document, - capabilities -> LSPIJUtils.hasCapability(capabilities.getDocumentHighlightProvider())) + + CompletableFuture future = LanguageServiceAccessor.getInstance(editor.getProject()) + .getLanguageServers(document, capabilities -> LSPIJUtils.hasCapability(capabilities.getDocumentHighlightProvider())) .thenAcceptAsync(languageServers -> - CompletableFuture.allOf(languageServers.stream() - .map(languageServer -> languageServer.getSecond().getTextDocumentService().documentHighlight(params)) + cancellationSupport.execute(CompletableFuture.allOf(languageServers.stream() + .map(languageServer -> cancellationSupport.execute(languageServer.getServer().getTextDocumentService().documentHighlight(params))) .map(request -> request.thenAcceptAsync(result -> { if (result != null) { result.forEach(hightlight -> highlights.add(hightlight)); } - })).toArray(CompletableFuture[]::new))); + })).toArray(CompletableFuture[]::new)))); while (!future.isDone() || !highlights.isEmpty()) { - ProgressManager.checkCanceled(); - DocumentHighlight highlight = highlights.poll(25, TimeUnit.MILLISECONDS); - if (highlight != null) { - TextRange textRange = LSPIJUtils.toTextRange(highlight.getRange(), document); - if (textRange != null) { - elements.add(new LSPHighlightPsiElement(textRange, highlight.getKind())); + ProgressManager.checkCanceled(); + DocumentHighlight highlight = highlights.poll(25, TimeUnit.MILLISECONDS); + if (highlight != null) { + TextRange textRange = LSPIJUtils.toTextRange(highlight.getRange(), document); + if (textRange != null) { + elements.add(new LSPHighlightPsiElement(textRange, highlight.getKind())); + } } - } } - } catch (ProcessCanceledException cancellation){ + } catch (ProcessCanceledException cancellation) { + cancellationSupport.cancel(); throw cancellation; } catch (InterruptedException e) { LOGGER.log(Level.WARNING, e, e::getLocalizedMessage); diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHover.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHover.java index 978886579..ada173cc0 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHover.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/hover/LSPTextHover.java @@ -21,6 +21,7 @@ import com.intellij.psi.PsiManager; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; import com.vladsch.flexmark.html.HtmlRenderer; import com.vladsch.flexmark.parser.Parser; import org.eclipse.lsp4j.Hover; @@ -54,6 +55,7 @@ public class LSPTextHover extends DocumentationProviderEx implements ExternalDoc private PsiElement lastElement; private int lastOffset = -1; private CompletableFuture> lspRequest; + private CancellationSupport cancellationSupport; public LSPTextHover() { LOGGER.info("LSPTextHover"); @@ -132,6 +134,8 @@ public String generateDoc(PsiElement element, @Nullable PsiElement originalEleme } catch (InterruptedException e) { LOGGER.warn(e.getLocalizedMessage(), e); Thread.currentThread().interrupt(); + } finally { + this.cancellationSupport = null; } return null; } @@ -181,29 +185,27 @@ private int getTargetOffset(PsiElement originalElement) { * @param offset the target offset. */ private void initiateHoverRequest(PsiElement element, int offset) { + if (this.cancellationSupport != null) { + this.cancellationSupport.cancel(); + this.cancellationSupport = null; + } PsiDocumentManager manager = PsiDocumentManager.getInstance(element.getProject()); final Document document = manager.getDocument(element.getContainingFile()); if (offset != -1 && (this.lspRequest == null || !element.equals(this.lastElement) || offset != this.lastOffset)) { this.lastElement = element; this.lastOffset = offset; + this.cancellationSupport = new CancellationSupport(); this.lspRequest = LanguageServiceAccessor.getInstance(element.getProject()) .getLanguageServers(document, capabilities -> isHoverCapable(capabilities)) .thenApplyAsync(languageServers -> // Async is very important here, otherwise the LS Client thread is in // deadlock and doesn't read bytes from LS languageServers.stream() - .map(languageServer -> { - try { - return languageServer.getSecond().getTextDocumentService() - .hover(LSPIJUtils.toHoverParams(offset, document)).get(); - } catch (ExecutionException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - return null; - } catch (InterruptedException e) { - LOGGER.warn(e.getLocalizedMessage(), e); - Thread.currentThread().interrupt(); - return null; - } - }).filter(Objects::nonNull).collect(Collectors.toList())); + .map(languageServer -> + cancellationSupport.execute( + languageServer.getServer().getTextDocumentService() + .hover(LSPIJUtils.toHoverParams(offset, document))) + .join() + ).filter(Objects::nonNull).collect(Collectors.toList())); } } diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/inlayhint/LSPInlayHintInlayProvider.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/inlayhint/LSPInlayHintInlayProvider.java index ad9348f21..e06283d8c 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/inlayhint/LSPInlayHintInlayProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/inlayhint/LSPInlayHintInlayProvider.java @@ -19,6 +19,7 @@ import com.intellij.codeInsight.hints.presentation.SequencePresentation; 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; @@ -27,6 +28,7 @@ import com.redhat.devtools.intellij.lsp4ij.AbstractLSPInlayProvider; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageServer; @@ -45,7 +47,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.TimeUnit; -import java.util.function.Function; import java.util.stream.Collectors; /** @@ -63,7 +64,13 @@ public InlayHintsCollector getCollectorFor(@NotNull PsiFile psiFile, return new FactoryInlayHintsCollector(editor) { @Override public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink) { + Project project = psiElement.getProject(); + if (project.isDisposed()) { + // The project has been closed, don't collect inlay hints. + return false; + } Document document = editor.getDocument(); + final CancellationSupport cancellationSupport = new CancellationSupport(); try { URI docURI = LSPIJUtils.toUri(document); if (docURI == null) { @@ -72,11 +79,17 @@ public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @ Range viewPortRange = getViewPortRange(editor); InlayHintParams param = new InlayHintParams(new TextDocumentIdentifier(docURI.toString()), viewPortRange); BlockingDeque> pairs = new LinkedBlockingDeque<>(); - CompletableFuture future = collect(psiElement.getProject(), document, param, pairs); + + CompletableFuture future = collect(psiElement.getProject(), document, param, pairs, cancellationSupport); List>> inlayHints = createInlayHints(document, pairs, future); - Map>>> elements = inlayHints.stream().collect(Collectors.groupingBy(p -> p.first)); - elements.forEach((offset, list) -> - inlayHintsSink.addInlineElement(offset, false, toPresentation(editor, list, getFactory()), false)); + inlayHints.stream() + .collect(Collectors.groupingBy(p -> p.first)) + .forEach((offset, list) -> + inlayHintsSink.addInlineElement(offset, false, toPresentation(editor, list, getFactory()), false)); + } catch (ProcessCanceledException e) { + // Cancel all LSP requests + cancellationSupport.cancel(); + throw e; } catch (InterruptedException e) { LOGGER.warn(e.getLocalizedMessage(), e); Thread.currentThread().interrupt(); @@ -102,19 +115,20 @@ private List>> createInlayHints( return inlayHints; } - private CompletableFuture collect(@NotNull Project project, @NotNull Document document, InlayHintParams param, BlockingDeque> pairs) { + private CompletableFuture collect(@NotNull Project project, @NotNull Document document, InlayHintParams param, BlockingDeque> pairs, CancellationSupport cancellationSupport) { return LanguageServiceAccessor.getInstance(project) .getLanguageServers(document, capabilities -> capabilities.getInlayHintProvider() != null) - .thenComposeAsync(languageServers -> CompletableFuture.allOf(languageServers.stream() - .map(languageServer -> languageServer.getSecond().getTextDocumentService().inlayHint(param) - .thenAcceptAsync(inlayHints -> { - // textDocument/codeLens may return null - if (inlayHints != null) { - inlayHints.stream().filter(Objects::nonNull) - .forEach(inlayHint -> pairs.add(new Pair(inlayHint, languageServer.getSecond()))); - } - })) - .toArray(CompletableFuture[]::new))); + .thenComposeAsync(languageServers -> cancellationSupport.execute(CompletableFuture.allOf(languageServers.stream() + .map(languageServer -> + cancellationSupport.execute(languageServer.getServer().getTextDocumentService().inlayHint(param)) + .thenAcceptAsync(inlayHints -> { + // textDocument/codeLens may return null + if (inlayHints != null) { + inlayHints.stream().filter(Objects::nonNull) + .forEach(inlayHint -> pairs.add(new Pair(inlayHint, languageServer.getServer()))); + } + })) + .toArray(CompletableFuture[]::new)))); } }; } @@ -165,7 +179,7 @@ private InlayPresentation createInlayPresentation( // InlayHintLabelPart defines a Command, create a clickable inlay hint int finalIndex = index; text = factory.referenceOnHover(text, (event, translated) -> - executeClientCommand(p.second.second, p.second.first, finalIndex, (Component) event.getSource(), project) + executeClientCommand(p.second.second, p.second.first, finalIndex, (Component) event.getSource(), project) ); } return text; diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/navigation/LSPGotoDeclarationHandler.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/navigation/LSPGotoDeclarationHandler.java index f11b9e107..ba8959ebe 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/navigation/LSPGotoDeclarationHandler.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/operations/navigation/LSPGotoDeclarationHandler.java @@ -23,6 +23,7 @@ import com.intellij.psi.PsiManager; import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils; import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; import com.redhat.devtools.intellij.lsp4mp4ij.psi.internal.core.ls.PsiUtilsLSImpl; import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.Location; @@ -56,11 +57,20 @@ public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement if (uri != null) { DefinitionParams params = new DefinitionParams(LSPIJUtils.toTextDocumentIdentifier(uri), LSPIJUtils.toPosition(offset, editor.getDocument())); Set targets = new HashSet<>(); + final CancellationSupport cancellationSupport = new CancellationSupport(); try { - LanguageServiceAccessor.getInstance(editor.getProject()).getLanguageServers(editor.getDocument(), capabilities -> LSPIJUtils.hasCapability(capabilities.getDefinitionProvider())).thenComposeAsync(servers -> - - CompletableFuture.allOf(servers.stream().map(server -> server.getSecond().getTextDocumentService().definition(params).thenAcceptAsync(definitions -> targets.addAll(toElements(editor.getProject(), definitions)))) - .toArray(CompletableFuture[]::new))).get(1_000, TimeUnit.MILLISECONDS); + LanguageServiceAccessor.getInstance(editor.getProject()) + .getLanguageServers(editor.getDocument(), capabilities -> LSPIJUtils.hasCapability(capabilities.getDefinitionProvider())) + .thenComposeAsync(languageServers -> + cancellationSupport.execute( + CompletableFuture.allOf( + languageServers + .stream() + .map(server -> + cancellationSupport.execute(server.getServer().getTextDocumentService().definition(params)) + .thenAcceptAsync(definitions -> targets.addAll(toElements(editor.getProject(), definitions)))) + .toArray(CompletableFuture[]::new)))) + .get(1_000, TimeUnit.MILLISECONDS); } catch (ExecutionException | TimeoutException e) { LOGGER.warn(e.getLocalizedMessage(), e); } @@ -74,7 +84,7 @@ public PsiElement[] getGotoDeclarationTargets(@Nullable PsiElement sourceElement } private List toElements(Project project, Either, List> definitions) { - List locations = definitions!=null?toLocation(definitions): Collections.emptyList(); + List locations = definitions != null ? toLocation(definitions) : Collections.emptyList(); return locations.stream().map(location -> toElement(project, location)).filter(Objects::nonNull).collect(Collectors.toList()); }