diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPInlayHintsProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPInlayHintsProvider.java index 645c5b9bc..0d8763816 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPInlayHintsProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/AbstractLSPInlayHintsProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 Red Hat, Inc. + * Copyright (c) 2022-2025 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, @@ -11,11 +11,13 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features; +import com.google.common.collect.Sets; import com.intellij.codeInsight.hints.*; import com.intellij.codeInsight.hints.presentation.PresentationFactory; import com.intellij.lang.Language; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.ui.layout.LCFlags; @@ -34,8 +36,7 @@ import javax.swing.*; import java.awt.*; import java.awt.event.InputEvent; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; @@ -49,6 +50,8 @@ public abstract class AbstractLSPInlayHintsProvider implements InlayHintsProvide return true; }; + public static final Key>> INLAY_HINTS_PENDING_FUTURES_KEY = Key.create("LSP_INLAY_HINTS_PENDING_FUTURES"); + private final SettingsKey key = new SettingsKey<>("LSP.hints"); @@ -78,8 +81,10 @@ public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @ } try { - final List> pendingFutures = new ArrayList<>(); - doCollect(psiFile, editor, getFactory(), inlayHintsSink, pendingFutures); + final Set> pendingFutures = Sets.newIdentityHashSet(); + editor.putUserData(INLAY_HINTS_PENDING_FUTURES_KEY, pendingFutures); + doCollect(psiFile, editor, getFactory(), inlayHintsSink); + editor.putUserData(INLAY_HINTS_PENDING_FUTURES_KEY, null); if (!pendingFutures.isEmpty()) { // Some LSP requests: // - textDocument/colorInformation @@ -160,7 +165,6 @@ protected void executeCommand(@Nullable Command command, protected abstract void doCollect(@NotNull PsiFile psiFile, @NotNull Editor editor, @NotNull PresentationFactory factory, - @NotNull InlayHintsSink inlayHintsSink, - @NotNull List> pendingFutures); + @NotNull InlayHintsSink inlayHintsSink); } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/color/LSPColorProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/color/LSPColorProvider.java index ec7fa333d..2d01a46ed 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/color/LSPColorProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/color/LSPColorProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2024 Red Hat, Inc. + * Copyright (c) 2024-2025 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, @@ -7,6 +7,7 @@ * * Contributors: * Red Hat, Inc. - initial API and implementation + * FalsePattern - Non-blocking LSP request ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.color; @@ -31,9 +32,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally; @@ -50,8 +53,7 @@ public class LSPColorProvider extends AbstractLSPInlayHintsProvider { protected void doCollect(@NotNull PsiFile psiFile, @NotNull Editor editor, @NotNull PresentationFactory factory, - @NotNull InlayHintsSink inlayHintsSink, - @NotNull List> pendingFutures) { + @NotNull InlayHintsSink inlayHintsSink) { // Get LSP color information from cache or create them LSPColorSupport colorSupport = LSPFileSupport.getSupport(psiFile).getColorSupport(); var params = new DocumentColorParams(LSPIJUtils.toTextDocumentIdentifier(psiFile.getVirtualFile())); @@ -59,7 +61,7 @@ protected void doCollect(@NotNull PsiFile psiFile, try { // Wait until the future is finished and stop the wait if there are some ProcessCanceledException. - waitUntilDone(future, psiFile); + waitUntilDone(future, psiFile, 25); if (isDoneNormally(future)) { // Collect color information @@ -80,13 +82,21 @@ protected void doCollect(@NotNull PsiFile psiFile, } catch (ProcessCanceledException ignore) {//Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility //TODO delete block when minimum required version is 2024.2 } catch (CancellationException ignore) { + } catch (TimeoutException ignore) { + var pendingFutures = editor.getUserData(INLAY_HINTS_PENDING_FUTURES_KEY); + if (pendingFutures != null) { + pendingFutures.add(future); + } } catch (ExecutionException e) { LOGGER.error("Error while consuming LSP 'textDocument/color' request", e); } finally { if (!future.isDone()) { // the future which collects all textDocument/colorInformation for all servers is not finished // add it to the pending futures to refresh again the UI when this future will be finished. - pendingFutures.add(future); + var pendingFutures = editor.getUserData(INLAY_HINTS_PENDING_FUTURES_KEY); + if (pendingFutures != null) { + pendingFutures.add(future); + } } } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/AbstractLSPDeclarativeInlayHintsProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/AbstractLSPDeclarativeInlayHintsProvider.java index c79801ea2..32b338310 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/AbstractLSPDeclarativeInlayHintsProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/AbstractLSPDeclarativeInlayHintsProvider.java @@ -10,9 +10,11 @@ ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.inlayhint; +import com.google.common.collect.Sets; import com.intellij.codeInsight.hints.declarative.*; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Key; import com.intellij.psi.PsiFile; import com.redhat.devtools.lsp4ij.LanguageServerItem; import com.redhat.devtools.lsp4ij.client.ExecuteLSPFeatureStatus; @@ -27,8 +29,7 @@ import java.awt.*; import java.awt.event.InputEvent; -import java.util.ArrayList; -import java.util.List; +import java.util.Set; import java.util.concurrent.CompletableFuture; /* @@ -40,6 +41,8 @@ public abstract class AbstractLSPDeclarativeInlayHintsProvider implements InlayH // Do nothing }; + public static final Key>> DECLARATIVE_INLAY_HINTS_PENDING_FUTURES_KEY = Key.create("LSP_DECLARATIVE_INLAY_HINTS_PENDING_FUTURES"); + @Override public @Nullable InlayHintsCollector createCollector(@NotNull PsiFile psiFile, @NotNull Editor editor) { if (ProjectIndexingManager.canExecuteLSPFeature(psiFile) != ExecuteLSPFeatureStatus.NOW) { @@ -66,8 +69,7 @@ protected void executeCommand(@Nullable Command command, protected abstract void doCollect(@NotNull PsiFile psiFile, @NotNull Editor editor, - @NotNull InlayTreeSink inlayHintsSink, - @NotNull List> pendingFutures); + @NotNull InlayTreeSink inlayHintsSink); private class Collector implements OwnBypassCollector { @@ -87,8 +89,10 @@ public void collectHintsForFile(@NotNull PsiFile psiFile, @NotNull InlayTreeSink return; } - final List> pendingFutures = new ArrayList<>(); - doCollect(psiFile, editor, inlayTreeSink, pendingFutures); + final Set> pendingFutures = Sets.newIdentityHashSet(); + editor.putUserData(DECLARATIVE_INLAY_HINTS_PENDING_FUTURES_KEY, pendingFutures); + doCollect(psiFile, editor, inlayTreeSink); + editor.putUserData(DECLARATIVE_INLAY_HINTS_PENDING_FUTURES_KEY, null); if (!pendingFutures.isEmpty()) { // Some LSP requests: // - textDocument/inlayHint, inlayHint/resolve diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/LSPInlayHintsProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/LSPInlayHintsProvider.java index 00860dc10..1d89cc23b 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/LSPInlayHintsProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/inlayhint/LSPInlayHintsProvider.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 Red Hat, Inc. + * Copyright (c) 2022-2025 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, @@ -7,7 +7,7 @@ * * Contributors: * Red Hat, Inc. - initial API and implementation - * FalsePattern - port to declarative inlay hint API + * FalsePattern - port to declarative inlay hint API, non-blocking LSP request ******************************************************************************/ package com.redhat.devtools.lsp4ij.features.inlayhint; @@ -33,6 +33,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -51,8 +52,7 @@ public class LSPInlayHintsProvider extends AbstractLSPDeclarativeInlayHintsProvi @Override protected void doCollect(@NotNull PsiFile psiFile, @NotNull Editor editor, - @NotNull InlayTreeSink inlayHintsSink, - @NotNull List> pendingFutures) { + @NotNull InlayTreeSink inlayHintsSink) { // Get LSP inlay hints from cache or create them LSPInlayHintsSupport inlayHintSupport = LSPFileSupport.getSupport(psiFile).getInlayHintsSupport(); Range viewPortRange = getViewPortRange(editor); @@ -61,7 +61,7 @@ protected void doCollect(@NotNull PsiFile psiFile, try { // Wait until the future is finished and stop the wait if there are some ProcessCanceledException. - waitUntilDone(future, psiFile); + waitUntilDone(future, psiFile, 25); if (isDoneNormally(future)) { // Collect inlay hints @@ -81,13 +81,21 @@ protected void doCollect(@NotNull PsiFile psiFile, } catch (ProcessCanceledException ignore) {//Since 2024.2 ProcessCanceledException extends CancellationException so we can't use multicatch to keep backward compatibility //TODO delete block when minimum required version is 2024.2 } catch (CancellationException ignore) { + } catch (TimeoutException ignore) { + var pendingFutures = editor.getUserData(DECLARATIVE_INLAY_HINTS_PENDING_FUTURES_KEY); + if (pendingFutures != null) { + pendingFutures.add(future); + } } catch (ExecutionException e) { LOGGER.error("Error while consuming LSP 'textDocument/inlayHint' request", e); } finally { if (!future.isDone()) { // the future which collects all textDocument/inlayHint for all servers is not finished // add it to the pending futures to refresh again the UI when this future will be finished. - pendingFutures.add(future); + var pendingFutures = editor.getUserData(DECLARATIVE_INLAY_HINTS_PENDING_FUTURES_KEY); + if (pendingFutures != null) { + pendingFutures.add(future); + } } } } diff --git a/src/main/java/com/redhat/devtools/lsp4ij/internal/CompletableFutures.java b/src/main/java/com/redhat/devtools/lsp4ij/internal/CompletableFutures.java index 716ccb7b8..75dd73796 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/internal/CompletableFutures.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/internal/CompletableFutures.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2023 Red Hat Inc. and others. + * Copyright (c) 2023-2025 Red Hat Inc. and others. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0 which is available at @@ -10,6 +10,7 @@ * * Contributors: * Red Hat Inc. - initial API and implementation + * FalsePattern - Added waitUntilDoneOrTimeout *******************************************************************************/ package com.redhat.devtools.lsp4ij.internal; @@ -99,18 +100,38 @@ public static void waitUntilDone(@NotNull CompletableFuture future) throws Ex } /** - * Wait for the done of the given future and stop the wait if {@link ProcessCanceledException} is thrown. - * - * @param future the future to wait. - * @param file the Psi file. + * Equivalent to {@link #waitUntilDone(CompletableFuture, PsiFile, Integer)}, but with a null timeout. + * Convenience function that strips the {@link TimeoutException} that never gets thrown. */ public static void waitUntilDone(@Nullable CompletableFuture future, @Nullable PsiFile file) throws ExecutionException, ProcessCanceledException { + try { + waitUntilDone(future, file, null); + } catch (TimeoutException e) { + // This can never happen + throw new AssertionError(e); + } + } + + /** + * Waits for the given future to complete, and stops the wait if {@link ProcessCanceledException} is thrown. + * + * @param future the future to wait. + * @param file the Psi file. + * @param timeout the amount of time to wait in milliseconds for the future to complete, and if the timeout is + * reached, this method throws {@link TimeoutException}. If null, this method waits indefinitely + * without ever throwing that exception. + */ + public static void waitUntilDone(@Nullable CompletableFuture future, + @Nullable PsiFile file, + @Nullable Integer timeout) throws ExecutionException, ProcessCanceledException, TimeoutException { if (future == null) { return; } long start = System.currentTimeMillis(); final long modificationStamp = file != null ? file.getModificationStamp() : -1; + boolean throwTimeout = timeout != null; + int realTimeout = timeout != null ? timeout : 25; while (!future.isDone()) { try { // check progress canceled @@ -121,9 +142,9 @@ public static void waitUntilDone(@Nullable CompletableFuture future, throw new CancellationException("Psi file has changed."); } } - // wait for 25 ms - future.get(25, TimeUnit.MILLISECONDS); - } catch (TimeoutException ignore) { + // wait for timeout ms + future.get(realTimeout, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { if (file != null && !future.isDone() && System.currentTimeMillis() - start > 5000 && ProjectIndexingManager.isIndexingAll()){ // When some projects are being indexed, // the language server startup can take a long time @@ -132,6 +153,9 @@ public static void waitUntilDone(@Nullable CompletableFuture future, // This wait can block IJ, here we stop the wait (and we could lose some LSP feature) throw new CancellationException("Some projects are indexing"); } + if (throwTimeout) { + throw e; + } // Ignore timeout } catch (ExecutionException | CompletionException e) { Throwable cause = e.getCause(); @@ -151,7 +175,6 @@ public static void waitUntilDone(@Nullable CompletableFuture future, } } } - /** * Wait in Task (which is cancellable) for the done of the given future and stop the wait if {@link ProcessCanceledException} is thrown. * diff --git a/src/main/java/com/redhat/devtools/lsp4ij/internal/editor/EditorFeatureManager.java b/src/main/java/com/redhat/devtools/lsp4ij/internal/editor/EditorFeatureManager.java index cd217c611..d9f96ada1 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/internal/editor/EditorFeatureManager.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/internal/editor/EditorFeatureManager.java @@ -28,6 +28,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.CompletableFuture; @@ -127,7 +128,7 @@ public void refreshEditorFeature(@NotNull VirtualFile file, * @param psiFile the file to check the timestamp of * @param feature the editor feature to refresh */ - public void refreshEditorFeatureWhenAllDone(@NotNull List> pendingFutures, + public void refreshEditorFeatureWhenAllDone(@NotNull Set> pendingFutures, long modificationStamp, @NotNull PsiFile psiFile, @NotNull EditorFeatureType feature) {