Skip to content

Commit

Permalink
feat: Non-blocking LSP requests for inlay hints and color hints
Browse files Browse the repository at this point in the history
  • Loading branch information
FalsePattern committed Feb 11, 2025
1 parent 11da5af commit deb3752
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 34 deletions.
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

0 comments on commit deb3752

Please sign in to comment.