From 2d1cee63fbed0785cdfd2376563f461b35d030ec Mon Sep 17 00:00:00 2001 From: azerr Date: Sun, 26 Nov 2023 22:36:31 +0100 Subject: [PATCH] fix: duplicate inlay hint Signed-off-by: azerr --- .../lsp4ij/AbstractLSPInlayProvider.java | 93 ++++++++---- .../codelens/LSPCodelensInlayProvider.java | 141 +++++++----------- .../inlayhint/LSPInlayHintInlayProvider.java | 126 ++++++---------- 3 files changed, 169 insertions(+), 191 deletions(-) diff --git a/src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractLSPInlayProvider.java b/src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractLSPInlayProvider.java index 436ece3d6..185685f55 100644 --- a/src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractLSPInlayProvider.java +++ b/src/main/java/com/redhat/devtools/intellij/lsp4ij/AbstractLSPInlayProvider.java @@ -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; @@ -29,23 +26,78 @@ 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 { - private final Key sinkKey; + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractLSPInlayProvider.class); - protected AbstractLSPInlayProvider(Key sinkKey) { - this.sinkKey = sinkKey; + private final Key cancellationSupportKey; + + protected AbstractLSPInlayProvider(Key cancellationSupportKey) { + this.cancellationSupportKey = cancellationSupportKey; } private SettingsKey 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); + } 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; @@ -106,31 +158,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); } } 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 d88aa14fc..bbc3bdffa 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 @@ -10,34 +10,28 @@ ******************************************************************************/ 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; import com.redhat.devtools.intellij.lsp4ij.internal.CancellationSupport; +import com.redhat.devtools.intellij.lsp4ij.operations.inlayhint.LSPInlayHintInlayProvider; import org.eclipse.lsp4j.CodeLens; import org.eclipse.lsp4j.CodeLensParams; import org.eclipse.lsp4j.Command; 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; @@ -58,94 +52,69 @@ public class LSPCodelensInlayProvider extends AbstractLSPInlayProvider { private static final Logger LOGGER = LoggerFactory.getLogger(LSPCodelensInlayProvider.class); - private static final Key SINK_KEY = new Key<>(LSPCodelensInlayProvider.class.getName()); + private static final Key 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> 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> pairs = new LinkedBlockingDeque<>(); - CompletableFuture future = collect(file, psiFile.getProject(), 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(); - } catch (InterruptedException e) { - // Cancel all LSP requests - cancellationSupport.cancel(); - LOGGER.warn(e.getLocalizedMessage(), e); - Thread.currentThread().interrupt(); - } - return false; - } + CompletableFuture future = collect(file, 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, factory)) + ); + } - @NotNull - 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)); - } - } - return codelenses; + @NotNull + 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)); } + } + return codelenses; + } - private @NotNull CompletableFuture collect(@NotNull VirtualFile file, @NotNull Project project, @NotNull CodeLensParams param, @NotNull BlockingDeque> 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 collect(@NotNull VirtualFile file, @NotNull Project project, @NotNull CodeLensParams param, @NotNull BlockingDeque> 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( 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 a9075be86..34d1a0005 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 @@ -10,23 +10,17 @@ ******************************************************************************/ package com.redhat.devtools.intellij.lsp4ij.operations.inlayhint; -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; @@ -35,7 +29,6 @@ import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageServer; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,86 +49,61 @@ public class LSPInlayHintInlayProvider extends AbstractLSPInlayProvider { private static final Logger LOGGER = LoggerFactory.getLogger(LSPInlayHintInlayProvider.class); - private static final Key SINK_KEY = new Key<>(LSPInlayHintInlayProvider.class.getName()); + private static final Key CANCELLATION_SUPPORT_KEY = new Key<>(LSPInlayHintInlayProvider.class.getName() + "-CancellationSupport"); public LSPInlayHintInlayProvider() { - 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) { - // InlayHint must not be collected - return false; - } - Document document = editor.getDocument(); - final CancellationSupport cancellationSupport = new CancellationSupport(); - try { - URI fileUri = LSPIJUtils.toUri(file); - Range viewPortRange = getViewPortRange(editor); - InlayHintParams param = new InlayHintParams(new TextDocumentIdentifier(fileUri.toASCIIString()), viewPortRange); - BlockingDeque> pairs = new LinkedBlockingDeque<>(); - - CompletableFuture future = collect(psiElement.getProject(), file, param, pairs, cancellationSupport); - List>> inlayHints = createInlayHints(document, pairs, future); - 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(); - } catch (InterruptedException e) { - // Cancel all LSP requests - cancellationSupport.cancel(); - LOGGER.warn(e.getLocalizedMessage(), e); - Thread.currentThread().interrupt(); - } - return false; - } + 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); + Range viewPortRange = getViewPortRange(editor); + InlayHintParams param = new InlayHintParams(new TextDocumentIdentifier(fileUri.toASCIIString()), viewPortRange); + BlockingDeque> pairs = new LinkedBlockingDeque<>(); + + CompletableFuture future = collect(project, file, param, pairs, cancellationSupport); + List>> inlayHints = createInlayHints(document, pairs, future); + inlayHints.stream() + .collect(Collectors.groupingBy(p -> p.first)) + .forEach((offset, list) -> + inlayHintsSink.addInlineElement(offset, false, toPresentation(editor, list, factory), false)); + } - @NotNull - private List>> createInlayHints( - @NotNull Document document, - BlockingDeque> pairs, - CompletableFuture future) - throws InterruptedException { - List>> inlayHints = 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().getPosition(), document); - inlayHints.add(Pair.create(offset, pair)); - } - } - return inlayHints; + @NotNull + private List>> createInlayHints( + @NotNull Document document, + BlockingDeque> pairs, + CompletableFuture future) + throws InterruptedException { + List>> inlayHints = 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().getPosition(), document); + inlayHints.add(Pair.create(offset, pair)); } + } + return inlayHints; + } - private CompletableFuture collect(@NotNull Project project, @NotNull VirtualFile file, InlayHintParams param, BlockingDeque> pairs, CancellationSupport cancellationSupport) { - return LanguageServiceAccessor.getInstance(project) - .getLanguageServers(file, capabilities -> capabilities.getInlayHintProvider() != null) - .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)))); - } - }; + private CompletableFuture collect(@NotNull Project project, @NotNull VirtualFile file, InlayHintParams param, BlockingDeque> pairs, CancellationSupport cancellationSupport) { + return LanguageServiceAccessor.getInstance(project) + .getLanguageServers(file, capabilities -> capabilities.getInlayHintProvider() != null) + .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)))); } @NotNull