Skip to content

Commit

Permalink
fix: duplicate inlay hint
Browse files Browse the repository at this point in the history
Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr authored and fbricon committed Nov 27, 2023
1 parent 5f1003d commit 7f4eb2a
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 204 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,13 @@
package com.redhat.devtools.intellij.lsp4ij;

import com.intellij.codeInsight.hints.*;
import com.intellij.codeInsight.hints.presentation.PresentationFactory;
import com.intellij.ide.DataManager;
import com.intellij.lang.Language;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.Presentation;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.vfs.VirtualFile;
Expand All @@ -29,23 +26,79 @@
import com.intellij.ui.layout.LCFlags;
import com.intellij.ui.layout.LayoutKt;
import com.redhat.devtools.intellij.lsp4ij.commands.CommandExecutor;
import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport;
import org.eclipse.lsp4j.Command;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.swing.JComponent;
import java.awt.Component;
import javax.swing.*;
import java.awt.*;

public abstract class AbstractLSPInlayProvider implements InlayHintsProvider<NoSettings> {

private final Key<InlayHintsSink> sinkKey;
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLSPInlayProvider.class);

protected AbstractLSPInlayProvider(Key<InlayHintsSink> sinkKey) {
this.sinkKey = sinkKey;
private final Key<CancellationSupport> cancellationSupportKey;

protected AbstractLSPInlayProvider(Key<CancellationSupport> cancellationSupportKey) {
this.cancellationSupportKey = cancellationSupportKey;
}

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


@Nullable
@Override
public final InlayHintsCollector getCollectorFor(@NotNull PsiFile psiFile,
@NotNull Editor editor,
@NotNull NoSettings o,
@NotNull InlayHintsSink inlayHintsSink) {
CancellationSupport previousCancellationSupport = editor.getUserData(cancellationSupportKey);
if (previousCancellationSupport != null) {
previousCancellationSupport.cancel();
}
CancellationSupport cancellationSupport = new CancellationSupport();
editor.putUserData(cancellationSupportKey, cancellationSupport);

return new FactoryInlayHintsCollector(editor) {

private boolean processed;

@Override
public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink) {
if (processed) {
// Before IJ 2023-3, FactoryInlayHintsCollector#collect(PsiElement element.. is called once time with PsiFile as element.
// Since IJ 2023-3, FactoryInlayHintsCollector#collect(PsiElement element.. is called several times for each token of the PsiFile
// which causes the problem of codelens/inlay hint which are not displayed because there are too many call of LSP request codelens/inlayhint which are cancelled.
// With IJ 2023-3 we need to collect LSP CodeLens/InlayHint just for the first call.
return false;
}
processed = true;
VirtualFile file = getFile(psiFile);
if (file == null) {
// InlayHint must not be collected
return false;
}
try {
doCollect(file, psiFile.getProject(), editor, getFactory(), inlayHintsSink, cancellationSupport);
cancellationSupport.checkCanceled();
} catch (ProcessCanceledException e) {
// Cancel all LSP requests
cancellationSupport.cancel();
} catch (InterruptedException e) {
// Cancel all LSP requests
cancellationSupport.cancel();
LOGGER.warn(e.getLocalizedMessage(), e);
Thread.currentThread().interrupt();
}
return false;
}
};
}


@Override
public boolean isVisibleInSettings() {
return true;
Expand Down Expand Up @@ -106,31 +159,20 @@ ActionPlaces.UNKNOWN, new Presentation(),
}
}

protected abstract void doCollect(@NotNull VirtualFile file, @NotNull Project project, @NotNull Editor editor, @NotNull PresentationFactory factory, @NotNull InlayHintsSink inlayHintsSink, @NotNull CancellationSupport cancellationSupport) throws InterruptedException;

/**
* Returns the virtual file where inlay hint must be added and null otherwise.
*
* @param psiFile the psi file.
* @param editor the editor.
* @param inlayHintsSink the inlay hints sink.
* @param psiFile the psi file.
* @return the virtual file where inlay hint must be added and null otherwise.
*/
protected @Nullable VirtualFile getFile(@NotNull PsiFile psiFile, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink) {
private @Nullable VirtualFile getFile(@NotNull PsiFile psiFile) {
Project project = psiFile.getProject();
if (project.isDisposed()) {
// The project has been closed, don't collect inlay hints.
return null;
}
// Before IJ 2023-3, FactoryInlayHintsCollector#collect(PsiElement element.. is called once time with PsiFile as element.
// Since IJ 2023-3, FactoryInlayHintsCollector#collect(PsiElement element.. is called several times for each tokens of the PsiFile
// which causes the problem of codelens/inlay hint which are not displayed because there are too many call of LSP request codelens/inlayhint which are cancelled.
// With IJ 2023-3 we need to collect LSP CodeLens/InlayHint just for the first call. To implement this idea, we store the instance InlayHintsSink,
// and we forbid the compute of inlay hint if InlayHintsSink is already filled.
InlayHintsSink sink = editor.getUserData(sinkKey);
if (sink == inlayHintsSink) {
// LSP CodeLens/InlayHint has already be done for teh file, ignore it.
return null;
}
editor.putUserData(sinkKey, inlayHintsSink);
return LSPIJUtils.getFile(psiFile);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,17 @@
******************************************************************************/
package com.redhat.devtools.intellij.lsp4ij.operations.codelens;

import com.intellij.codeInsight.hints.FactoryInlayHintsCollector;
import com.intellij.codeInsight.hints.InlayHintsCollector;
import com.intellij.codeInsight.hints.InlayHintsSink;
import com.intellij.codeInsight.hints.NoSettings;
import com.intellij.codeInsight.hints.presentation.InlayPresentation;
import com.intellij.codeInsight.hints.presentation.PresentationFactory;
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.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.redhat.devtools.intellij.lsp4ij.AbstractLSPInlayProvider;
import com.redhat.devtools.intellij.lsp4ij.LSPIJUtils;
import com.redhat.devtools.intellij.lsp4ij.LanguageServiceAccessor;
Expand All @@ -37,9 +31,6 @@
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.services.LanguageServer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.net.URI;
Expand All @@ -56,109 +47,84 @@
* LSP textDocument/codeLens support.
*/
public class LSPCodelensInlayProvider extends AbstractLSPInlayProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(LSPCodelensInlayProvider.class);

private static final Key<InlayHintsSink> SINK_KEY = new Key<>(LSPCodelensInlayProvider.class.getName());
private static final Key<CancellationSupport> CANCELLATION_SUPPORT_KEY = new Key<>(LSPCodelensInlayProvider.class.getName() + "-CancellationSupport");


public LSPCodelensInlayProvider() {
super(SINK_KEY);
super(CANCELLATION_SUPPORT_KEY);
}

@Nullable
@Override
public InlayHintsCollector getCollectorFor(@NotNull PsiFile psiFile,
@NotNull Editor editor,
@NotNull NoSettings o,
@NotNull InlayHintsSink inlayHintsSink) {
return new FactoryInlayHintsCollector(editor) {
@Override
public boolean collect(@NotNull PsiElement psiElement, @NotNull Editor editor, @NotNull InlayHintsSink inlayHintsSink) {
VirtualFile file = getFile(psiFile, editor, inlayHintsSink);
if (file == null) {
// Codelens must not be collected
return false;
}
Document document = editor.getDocument();
final CancellationSupport cancellationSupport = new CancellationSupport();
try {
URI fileUri = LSPIJUtils.toUri(file);
CodeLensParams param = new CodeLensParams(new TextDocumentIdentifier(fileUri.toASCIIString()));
BlockingDeque<Pair<CodeLens, LanguageServer>> pairs = new LinkedBlockingDeque<>();
protected void doCollect(@NotNull VirtualFile file, @NotNull Project project, @NotNull Editor editor, @NotNull PresentationFactory factory, @NotNull InlayHintsSink inlayHintsSink, @NotNull CancellationSupport cancellationSupport) throws InterruptedException {
Document document = editor.getDocument();
URI fileUri = LSPIJUtils.toUri(file);
CodeLensParams param = new CodeLensParams(new TextDocumentIdentifier(fileUri.toASCIIString()));
BlockingDeque<Pair<CodeLens, LanguageServer>> pairs = new LinkedBlockingDeque<>();

CompletableFuture<Void> future = collect(file, psiFile.getProject(), param, pairs, cancellationSupport);
List<Pair<Integer, Pair<CodeLens, LanguageServer>>> 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();
} catch (InterruptedException e) {
// Cancel all LSP requests
cancellationSupport.cancel();
LOGGER.warn(e.getLocalizedMessage(), e);
Thread.currentThread().interrupt();
}
return false;
}
CompletableFuture<Void> future = collect(file, project, param, pairs, cancellationSupport);
List<Pair<Integer, Pair<CodeLens, LanguageServer>>> 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, factory, cancellationSupport))
);
}

@NotNull
private List<Pair<Integer, Pair<CodeLens, LanguageServer>>> createCodeLenses(Document document, BlockingDeque<Pair<CodeLens, LanguageServer>> pairs, CompletableFuture<Void> future, CancellationSupport cancellationSupport) throws InterruptedException {
List<Pair<Integer, Pair<CodeLens, LanguageServer>>> codelenses = new ArrayList<>();
while (!future.isDone() || !pairs.isEmpty()) {
ProgressManager.checkCanceled();
Pair<CodeLens, LanguageServer> pair = pairs.poll(25, TimeUnit.MILLISECONDS);
if (pair != null) {
int offset = LSPIJUtils.toOffset(pair.getFirst().getRange().getStart(), document);
codelenses.add(Pair.create(offset, pair));
}
}
return codelenses;
@NotNull
private List<Pair<Integer, Pair<CodeLens, LanguageServer>>> createCodeLenses(Document document, BlockingDeque<Pair<CodeLens, LanguageServer>> pairs, CompletableFuture<Void> future, CancellationSupport cancellationSupport) throws InterruptedException {
List<Pair<Integer, Pair<CodeLens, LanguageServer>>> codelenses = new ArrayList<>();
while (!future.isDone() || !pairs.isEmpty()) {
ProgressManager.checkCanceled();
Pair<CodeLens, LanguageServer> pair = pairs.poll(25, TimeUnit.MILLISECONDS);
if (pair != null) {
int offset = LSPIJUtils.toOffset(pair.getFirst().getRange().getStart(), document);
codelenses.add(Pair.create(offset, pair));
}
}
return codelenses;
}

private @NotNull CompletableFuture<Void> collect(@NotNull VirtualFile file, @NotNull Project project, @NotNull CodeLensParams param, @NotNull BlockingDeque<Pair<CodeLens, LanguageServer>> pairs, @NotNull CancellationSupport cancellationSupport) {
return LanguageServiceAccessor.getInstance(project)
.getLanguageServers(file, capabilities -> capabilities.getCodeLensProvider() != null)
.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))));
}
};
private @NotNull CompletableFuture<Void> collect(@NotNull VirtualFile file, @NotNull Project project, @NotNull CodeLensParams param, @NotNull BlockingDeque<Pair<CodeLens, LanguageServer>> pairs, @NotNull CancellationSupport cancellationSupport) {
return LanguageServiceAccessor.getInstance(project)
.getLanguageServers(file, capabilities -> capabilities.getCodeLensProvider() != null)
.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))));
}

private InlayPresentation toPresentation(
Editor editor,
@NotNull Editor editor,
int offset,
List<Pair<Integer, Pair<CodeLens, LanguageServer>>> elements,
PresentationFactory factory
) {
@NotNull List<Pair<Integer, Pair<CodeLens, LanguageServer>>> elements,
@NotNull PresentationFactory factory,
@NotNull CancellationSupport cancellationSupport) {
int line = editor.getDocument().getLineNumber(offset);
int column = offset - editor.getDocument().getLineStartOffset(line);
List<InlayPresentation> presentations = new ArrayList<>();
presentations.add(factory.textSpacePlaceholder(column, true));
elements.forEach(p -> {
cancellationSupport.checkCanceled();
CodeLens codeLens = p.second.first;
LanguageServer languageServer = p.second.second;
InlayPresentation text = factory.smallText(getCodeLensContent(codeLens));
Expand Down
Loading

0 comments on commit 7f4eb2a

Please sign in to comment.