Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Non-blocking LSP requests for inlay hints and color hints #836

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -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;

Expand All @@ -49,6 +50,8 @@ public abstract class AbstractLSPInlayHintsProvider implements InlayHintsProvide
return true;
};

public static final Key<Set<CompletableFuture<?>>> INLAY_HINTS_PENDING_FUTURES_KEY = Key.create("LSP_INLAY_HINTS_PENDING_FUTURES");

private final SettingsKey<NoSettings> key = new SettingsKey<>("LSP.hints");


Expand Down Expand Up @@ -78,8 +81,10 @@ public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @
}

try {
final List<CompletableFuture<?>> pendingFutures = new ArrayList<>();
doCollect(psiFile, editor, getFactory(), inlayHintsSink, pendingFutures);
final Set<CompletableFuture<?>> 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
Expand Down Expand Up @@ -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<CompletableFuture<?>> pendingFutures);
@NotNull InlayHintsSink inlayHintsSink);

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/*******************************************************************************
* 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,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
* FalsePattern - Non-blocking LSP request
******************************************************************************/
package com.redhat.devtools.lsp4ij.features.color;

Expand All @@ -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;
Expand All @@ -50,16 +53,15 @@ public class LSPColorProvider extends AbstractLSPInlayHintsProvider {
protected void doCollect(@NotNull PsiFile psiFile,
@NotNull Editor editor,
@NotNull PresentationFactory factory,
@NotNull InlayHintsSink inlayHintsSink,
@NotNull List<CompletableFuture<?>> 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()));
CompletableFuture<List<ColorData>> future = colorSupport.getColors(params);

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
Expand All @@ -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);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/*
Expand All @@ -40,6 +41,8 @@ public abstract class AbstractLSPDeclarativeInlayHintsProvider implements InlayH
// Do nothing
};

public static final Key<Set<CompletableFuture<?>>> 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) {
Expand All @@ -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<CompletableFuture<?>> pendingFutures);
@NotNull InlayTreeSink inlayHintsSink);


private class Collector implements OwnBypassCollector {
Expand All @@ -87,8 +89,10 @@ public void collectHintsForFile(@NotNull PsiFile psiFile, @NotNull InlayTreeSink
return;
}

final List<CompletableFuture<?>> pendingFutures = new ArrayList<>();
doCollect(psiFile, editor, inlayTreeSink, pendingFutures);
final Set<CompletableFuture<?>> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
/*******************************************************************************
* 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,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* 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;

Expand All @@ -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;

Expand All @@ -51,8 +52,7 @@ public class LSPInlayHintsProvider extends AbstractLSPDeclarativeInlayHintsProvi
@Override
protected void doCollect(@NotNull PsiFile psiFile,
@NotNull Editor editor,
@NotNull InlayTreeSink inlayHintsSink,
@NotNull List<CompletableFuture<?>> pendingFutures) {
@NotNull InlayTreeSink inlayHintsSink) {
// Get LSP inlay hints from cache or create them
LSPInlayHintsSupport inlayHintSupport = LSPFileSupport.getSupport(psiFile).getInlayHintsSupport();
Range viewPortRange = getViewPortRange(editor);
Expand All @@ -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
Expand All @@ -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);
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,6 +10,7 @@
*
* Contributors:
* Red Hat Inc. - initial API and implementation
* FalsePattern - Added waitUntilDoneOrTimeout
*******************************************************************************/
package com.redhat.devtools.lsp4ij.internal;

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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();
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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<CompletableFuture<?>> pendingFutures,
public void refreshEditorFeatureWhenAllDone(@NotNull Set<CompletableFuture<?>> pendingFutures,
long modificationStamp,
@NotNull PsiFile psiFile,
@NotNull EditorFeatureType feature) {
Expand Down
Loading