From 6511cf48aee8489be2a1b164f160e13c7cdc8a60 Mon Sep 17 00:00:00 2001 From: azerr Date: Sat, 22 Jun 2024 20:25:57 +0200 Subject: [PATCH] feat: Support for `textDocument/semanticTokens` Fixes #238 Signed-off-by: azerr --- .../devtools/lsp4ij/LSPFileSupport.java | 16 +- .../redhat/devtools/lsp4ij/LSPIJUtils.java | 7 +- .../devtools/lsp4ij/LSPRequestConstants.java | 1 + .../devtools/lsp4ij/LanguageServerItem.java | 14 ++ .../lsp4ij/client/LanguageClientImpl.java | 22 +++ .../LSPDocumentationTargetProvider.java | 2 - .../DefaultSemanticTokensColorsProvider.java | 163 ++++++++++++++++++ .../LSPSemanticTokensRainbowVisitor.java | 85 +++++++++ .../LSPSemanticTokensSupport.java | 100 +++++++++++ .../SemanticTokensColorSettingsPage.java | 97 +++++++++++ .../SemanticTokensColorsProvider.java | 16 ++ .../semanticTokens/SemanticTokensData.java | 114 ++++++++++++ .../SemanticTokensHighlightingColors.java | 43 +++++ .../internal/ClientCapabilitiesFactory.java | 40 +++++ .../definition/LanguageServerDefinition.java | 6 + src/main/resources/META-INF/plugin.xml | 6 + .../messages/LanguageServerBundle.properties | 23 ++- .../gopls/initializationOptions.json | 3 + 18 files changed, 752 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/DefaultSemanticTokensColorsProvider.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensRainbowVisitor.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensSupport.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensColorSettingsPage.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensColorsProvider.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensData.java create mode 100644 src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensHighlightingColors.java create mode 100644 src/main/resources/templates/gopls/initializationOptions.json diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java index 4728bc5f7..d06ba28c9 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java @@ -29,6 +29,7 @@ import com.redhat.devtools.lsp4ij.features.references.LSPReferenceSupport; import com.redhat.devtools.lsp4ij.features.rename.LSPPrepareRenameSupport; import com.redhat.devtools.lsp4ij.features.rename.LSPRenameSupport; +import com.redhat.devtools.lsp4ij.features.semanticTokens.LSPSemanticTokensSupport; import com.redhat.devtools.lsp4ij.features.signatureHelp.LSPSignatureHelpSupport; import com.redhat.devtools.lsp4ij.features.typeDefinition.LSPTypeDefinitionSupport; import org.jetbrains.annotations.ApiStatus; @@ -78,6 +79,8 @@ public class LSPFileSupport extends UserDataHolderBase implements Disposable { private final LSPTypeDefinitionSupport typeDefinitionSupport; + private final LSPSemanticTokensSupport semanticTokensSupport; + private LSPFileSupport(@NotNull PsiFile file) { this.file = file; this.codeLensSupport = new LSPCodeLensSupport(file); @@ -97,6 +100,7 @@ private LSPFileSupport(@NotNull PsiFile file) { this.referenceSupport = new LSPReferenceSupport(file); this.declarationSupport = new LSPDeclarationSupport(file); this.typeDefinitionSupport = new LSPTypeDefinitionSupport(file); + this.semanticTokensSupport = new LSPSemanticTokensSupport(file); file.putUserData(LSP_FILE_SUPPORT_KEY, this); } @@ -121,8 +125,9 @@ public void dispose() { getReferenceSupport().cancel(); getDeclarationSupport().cancel(); getTypeDefinitionSupport().cancel(); + getSemanticTokensSupport().cancel(); var map = getUserMap(); - for(var key : map.getKeys()) { + for (var key : map.getKeys()) { var value = map.get(key); if (value instanceof Disposable disposable) { disposable.dispose(); @@ -283,6 +288,15 @@ public LSPTypeDefinitionSupport getTypeDefinitionSupport() { return typeDefinitionSupport; } + /** + * Returns the LSP semantic tokens support. + * + * @return the LSP semantic tokens support. + */ + public LSPSemanticTokensSupport getSemanticTokensSupport() { + return semanticTokensSupport; + } + /** * Return the existing LSP file support for the given Psi file, or create a new one if necessary. * diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java index 01dd20601..6117991bf 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java @@ -484,10 +484,13 @@ public static Module getModule(@Nullable VirtualFile file, @NotNull Project proj * @return a valid offset from the given position in the given document. */ public static int toOffset(@NotNull Position position, @NotNull Document document) { + return toOffset(position.getLine(), position.getCharacter(), document); + } + + public static int toOffset(int line, int character, @NotNull Document document) { // See https://github.com/microsoft/vscode-languageserver-node/blob/8e625564b531da607859b8cb982abb7cdb2fbe2e/textDocument/src/main.ts#L304 // Adjust position line/character according to this comment https://github.com/microsoft/vscode-languageserver-node/blob/ed3cd0f78c1495913bda7318ace2be7f968008af/textDocument/src/main.ts#L26 - int line = position.getLine(); if (line >= document.getLineCount()) { // The line number is greater than the number of lines in a document, it defaults back to the number of lines in the document. return document.getTextLength(); @@ -498,7 +501,7 @@ public static int toOffset(@NotNull Position position, @NotNull Document documen int lineOffset = document.getLineStartOffset(line); int nextLineOffset = document.getLineEndOffset(line); // If the character value is greater than the line length it defaults back to the line length - return Math.max(Math.min(lineOffset + position.getCharacter(), nextLineOffset), lineOffset); + return Math.max(Math.min(lineOffset + character, nextLineOffset), lineOffset); } /** diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LSPRequestConstants.java b/src/main/java/com/redhat/devtools/lsp4ij/LSPRequestConstants.java index 28d07f053..94535b03e 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LSPRequestConstants.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LSPRequestConstants.java @@ -27,6 +27,7 @@ public class LSPRequestConstants { public static final String TEXT_DOCUMENT_DEFINITION = "textDocument/definition"; public static final String TEXT_DOCUMENT_DOCUMENT_LINK = "textDocument/documentLink"; public static final String TEXT_DOCUMENT_FOLDING_RANGE = "textDocument/foldingRange"; + public static final String TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL = "textDocument/semanticTokensFull"; public static final String TEXT_DOCUMENT_TYPE_DEFINITION = "textDocument/typeDefinition"; public static final String TEXT_DOCUMENT_CODE_ACTION = "textDocument/codeAction"; public static final String TEXT_DOCUMENT_CODE_LENS = "textDocument/codeLens"; diff --git a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java index 85571149e..dd4f96b82 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java @@ -11,6 +11,7 @@ package com.redhat.devtools.lsp4ij; import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.features.semanticTokens.SemanticTokensColorsProvider; import org.eclipse.lsp4j.*; import org.eclipse.lsp4j.jsonrpc.messages.Either; import org.eclipse.lsp4j.services.LanguageServer; @@ -405,6 +406,16 @@ public static boolean isCodeActionResolveSupported(@Nullable ServerCapabilities return false; } + /** + * Returns true if the language server can support semantic tokens and false otherwise. + * + * @param serverCapabilities the server capabilities. + * @return true if the language server can support semantic tokens and false otherwise. + */ + public static boolean isSemanticTokensSupported(@Nullable ServerCapabilities serverCapabilities) { + return serverCapabilities != null && + serverCapabilities.getSemanticTokensProvider() != null; + } /** * Returns true if the language server can support rename and false otherwise. @@ -498,4 +509,7 @@ private static boolean hasCapability(Boolean capability) { return capability != null && capability; } + public SemanticTokensColorsProvider getSemanticTokensColorsProvider() { + return getServerWrapper().getServerDefinition().getSemanticTokensColorsProvider(); + } } \ No newline at end of file diff --git a/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java b/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java index 9e400e3ee..f0b4a9b65 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/client/LanguageClientImpl.java @@ -11,6 +11,7 @@ package com.redhat.devtools.lsp4ij.client; import com.google.gson.JsonObject; +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; import com.intellij.openapi.Disposable; import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.editor.Editor; @@ -150,6 +151,27 @@ private void refreshInlayHintsForAllOpenedFiles() { } } + @Override + public CompletableFuture refreshSemanticTokens() { + return CompletableFuture.runAsync(() -> { + if (wrapper == null) { + return; + } + refreshSemanticTokensForAllOpenedFiles(); + }); + } + + private void refreshSemanticTokensForAllOpenedFiles() { + for (var fileData : wrapper.getConnectedFiles()) { + VirtualFile file = fileData.getFile(); + final PsiFile psiFile = LSPIJUtils.getPsiFile(file, project); + if (psiFile != null) { + // Should be enough? + DaemonCodeAnalyzer.getInstance(psiFile.getProject()).restart(psiFile); + } + } + } + @Override public CompletableFuture createProgress(WorkDoneProgressCreateParams params) { return progressManager.createProgress(params); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTargetProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTargetProvider.java index 22448e2ff..57a831f74 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTargetProvider.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/documentation/LSPDocumentationTargetProvider.java @@ -11,7 +11,6 @@ package com.redhat.devtools.lsp4ij.features.documentation; import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.Editor; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.project.DumbService; import com.intellij.openapi.project.Project; @@ -83,7 +82,6 @@ public class LSPDocumentationTargetProvider implements DocumentationTargetProvid // textDocument/hover has been collected correctly List hovers = hoverFuture.getNow(null); if (hovers != null) { - Editor editor = LSPIJUtils.editorForElement(psiFile); return hovers .stream() .map(hover -> toDocumentTarget(hover, document, psiFile)) diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/DefaultSemanticTokensColorsProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/DefaultSemanticTokensColorsProvider.java new file mode 100644 index 000000000..698058deb --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/DefaultSemanticTokensColorsProvider.java @@ -0,0 +1,163 @@ +package com.redhat.devtools.lsp4ij.features.semanticTokens; + +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.psi.PsiFile; +import org.eclipse.lsp4j.SemanticTokenModifiers; +import org.eclipse.lsp4j.SemanticTokenTypes; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public class DefaultSemanticTokensColorsProvider implements SemanticTokensColorsProvider { + + + @Override + public @Nullable TextAttributesKey getTextAttributesKey(@NotNull String tokenType, + @NotNull List tokenModifiers, + @NotNull PsiFile file) { + switch (tokenType) { + + // class + case SemanticTokenTypes.Class: + return SemanticTokensHighlightingColors.CLASS; + + // comment + case SemanticTokenTypes.Comment: + return SemanticTokensHighlightingColors.COMMENT; + + // decorator + case SemanticTokenTypes.Decorator: + return SemanticTokensHighlightingColors.DECORATOR; + + // enum + case SemanticTokenTypes.Enum: + return SemanticTokensHighlightingColors.ENUM; + + // enum member + case SemanticTokenTypes.EnumMember: + return SemanticTokensHighlightingColors.ENUM_MEMBER; + + // event + case SemanticTokenTypes.Event: + return SemanticTokensHighlightingColors.EVENT; + + // function + case SemanticTokenTypes.Function: + if (hasTokenModifiers(tokenModifiers, + SemanticTokenModifiers.Declaration, + SemanticTokenModifiers.Definition)) { + // with declaration, definition modifiers + return SemanticTokensHighlightingColors.FUNCTION_DECLARATION; + } + // with other modifiers + return SemanticTokensHighlightingColors.FUNCTION_CALL; + + // interface + case SemanticTokenTypes.Interface: + return DefaultLanguageHighlighterColors.INTERFACE_NAME; + + // keyword + case SemanticTokenTypes.Keyword: + return SemanticTokensHighlightingColors.KEYWORD; + + // macro + case SemanticTokenTypes.Macro: + return SemanticTokensHighlightingColors.MACRO; + + // method + case SemanticTokenTypes.Method: { + if (hasTokenModifiers(tokenModifiers, + SemanticTokenModifiers.Declaration, + SemanticTokenModifiers.Definition)) { + // with declaration, definition modifiers + return SemanticTokensHighlightingColors.METHOD_DECLARATION; + } + // with other modifiers + return SemanticTokensHighlightingColors.METHOD_CALL; + } + + + /*case "member": + return SemanticTokensHighlightingColors.MEMBER_ATTRIBUTES; +*/ + + // modifier + case SemanticTokenTypes.Modifier: + return SemanticTokensHighlightingColors.MODIFIER; + + // namespace + case SemanticTokenTypes.Namespace: + return DefaultLanguageHighlighterColors.METADATA; + + // Number + case SemanticTokenTypes.Number: + return SemanticTokensHighlightingColors.NUMBER; + + // Operator + case SemanticTokenTypes.Operator: + return DefaultLanguageHighlighterColors.OPERATION_SIGN; + + // Parameter + case SemanticTokenTypes.Parameter: + return DefaultLanguageHighlighterColors.PARAMETER; + + // Property + case SemanticTokenTypes.Property: + if (hasTokenModifiers(tokenModifiers, SemanticTokenModifiers.Static)) { + if (hasTokenModifiers(tokenModifiers, SemanticTokenModifiers.Readonly)) { + // with static, readonly modifiers + return SemanticTokensHighlightingColors.STATIC_READONLY_PROPERTY; + } + // with static readonly modifiers + return SemanticTokensHighlightingColors.STATIC_PROPERTY; + } + if (hasTokenModifiers(tokenModifiers, SemanticTokenModifiers.Readonly)) { + // with readonly modifiers + return SemanticTokensHighlightingColors.READONLY_PROPERTY; + } + // with other modifiers + return SemanticTokensHighlightingColors.PROPERTY; + + case SemanticTokenTypes.Regexp: + return DefaultLanguageHighlighterColors.REASSIGNED_LOCAL_VARIABLE; + + case SemanticTokenTypes.String: + return SemanticTokensHighlightingColors.STRING; + + case SemanticTokenTypes.Struct: + return DefaultLanguageHighlighterColors.VALID_STRING_ESCAPE; + + case SemanticTokenTypes.Type: + return DefaultLanguageHighlighterColors.CLASS_REFERENCE; + + case SemanticTokenTypes.TypeParameter: + return DefaultLanguageHighlighterColors.MARKUP_ATTRIBUTE; + + // variable + case SemanticTokenTypes.Variable: + return DefaultLanguageHighlighterColors.LOCAL_VARIABLE; + } + return null; + } + + protected boolean hasTokenModifiers(List tokenModifiers, String... checkedTokenModifiers) { + if (tokenModifiers.isEmpty()) { + return false; + } + for (var modifier : checkedTokenModifiers) { + if (tokenModifiers.contains(modifier)) { + return true; + } + } + return false; + } + + protected boolean hasTokenModifiersOrEmpty(List tokenModifiers, String... checkedTokenModifiers) { + if (tokenModifiers.isEmpty()) { + return true; + } + return hasTokenModifiers(tokenModifiers, checkedTokenModifiers); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensRainbowVisitor.java b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensRainbowVisitor.java new file mode 100644 index 000000000..db66b5b5c --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensRainbowVisitor.java @@ -0,0 +1,85 @@ +package com.redhat.devtools.lsp4ij.features.semanticTokens; + +import com.intellij.codeInsight.daemon.RainbowVisitor; +import com.intellij.codeInsight.daemon.impl.HighlightVisitor; +import com.intellij.codeInsight.daemon.impl.analysis.HighlightInfoHolder; +import com.intellij.openapi.editor.colors.TextAttributesScheme; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPFileSupport; +import com.redhat.devtools.lsp4ij.LSPIJUtils; +import com.redhat.devtools.lsp4ij.LanguageServersRegistry; +import org.eclipse.lsp4j.SemanticTokensParams; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.isDoneNormally; +import static com.redhat.devtools.lsp4ij.internal.CompletableFutures.waitUntilDone; + +public class LSPSemanticTokensRainbowVisitor extends RainbowVisitor { + + private static final Logger LOGGER = LoggerFactory.getLogger(LSPSemanticTokensRainbowVisitor.class); + private TextAttributesScheme colorScheme; + + @Override + public boolean suitableForFile(@NotNull PsiFile file) { + return LanguageServersRegistry.getInstance().isFileSupported(file); + } + + @Override + public boolean analyze(@NotNull PsiFile file, boolean updateWholeFile, @NotNull HighlightInfoHolder holder, @NotNull Runnable action) { + this.colorScheme = holder.getColorsScheme(); + return super.analyze(file, updateWholeFile, holder, action); + } + + @Override + public void visit(@NotNull PsiElement element) { + // Consume LSP 'textDocument/semanticTokens' request + PsiFile file = element.getContainingFile(); + if (!LanguageServersRegistry.getInstance().isFileSupported(file)) { + return; + } + LSPSemanticTokensSupport semanticTokensSupport = LSPFileSupport.getSupport(file).getSemanticTokensSupport(); + var params = new SemanticTokensParams(LSPIJUtils.toTextDocumentIdentifier(file.getVirtualFile())); + CompletableFuture semanticTokensFuture = semanticTokensSupport.getSemanticTokens(params); + try { + waitUntilDone(semanticTokensFuture, file); + } catch ( + ProcessCanceledException e) {//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 + semanticTokensSupport.cancel(); + return; + } catch (CancellationException e) { + // cancel the LSP requests textDocument/foldingRanges + semanticTokensSupport.cancel(); + return; + } catch (ExecutionException e) { + LOGGER.error("Error while consuming LSP 'textDocument/semanticTokens' request", e); + return; + } + + if (isDoneNormally(semanticTokensFuture)) { + // textDocument/foldingRanges has been collected correctly, create list of IJ FoldingDescriptor from LSP FoldingRange list + SemanticTokensData semanticTokens = semanticTokensFuture.getNow(null); + if (semanticTokens != null) { + var document = LSPIJUtils.getDocument(file.getVirtualFile()); + if (document == null) { + return; + } + semanticTokens.highlight(super.getHighlighter(), file, document, info -> super.addInfo(info)); + } + + } + } + + @Override + public @NotNull HighlightVisitor clone() { + return new LSPSemanticTokensRainbowVisitor(); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensSupport.java b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensSupport.java new file mode 100644 index 000000000..b814bfa54 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/LSPSemanticTokensSupport.java @@ -0,0 +1,100 @@ +/******************************************************************************* + * Copyright (c) 2024 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 + ******************************************************************************/ +package com.redhat.devtools.lsp4ij.features.semanticTokens; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPRequestConstants; +import com.redhat.devtools.lsp4ij.LanguageServerItem; +import com.redhat.devtools.lsp4ij.LanguageServiceAccessor; +import com.redhat.devtools.lsp4ij.features.AbstractLSPFeatureSupport; +import com.redhat.devtools.lsp4ij.internal.CancellationSupport; +import org.eclipse.lsp4j.SemanticTokensLegend; +import org.eclipse.lsp4j.SemanticTokensParams; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +/** + * LSP semanticTokens support which loads and caches semantic tokens by consuming: + * + *
    + *
  • LSP 'textDocument/semanticTokens' requests
  • + *
+ */ +public class LSPSemanticTokensSupport extends AbstractLSPFeatureSupport { + + public LSPSemanticTokensSupport(@NotNull PsiFile file) { + super(file); + } + + public CompletableFuture getSemanticTokens(SemanticTokensParams params) { + return super.getFeatureData(params); + } + + @Override + protected CompletableFuture doLoad(SemanticTokensParams params, CancellationSupport cancellationSupport) { + PsiFile file = super.getFile(); + return getSemanticTokens(file.getVirtualFile(), file.getProject(), params, cancellationSupport); + } + + private static @NotNull CompletableFuture getSemanticTokens(@NotNull VirtualFile file, + @NotNull Project project, + @NotNull SemanticTokensParams params, + @NotNull CancellationSupport cancellationSupport) { + + return LanguageServiceAccessor.getInstance(project) + .getLanguageServers(file, LanguageServerItem::isSemanticTokensSupported) + .thenComposeAsync(languageServers -> { + // Here languageServers is the list of language servers which matches the given file + // and which have folding range capability + if (languageServers.isEmpty()) { + return CompletableFuture.completedFuture(null); + } + + // Collect list of textDocument/semanticTokens future for each language servers + List> semanticTokensPerServerFutures = languageServers + .stream() + .map(languageServer -> getSemanticTokensFor(params, languageServer, cancellationSupport)) + .filter(Objects::nonNull) + .toList(); + + // Merge list of textDocument/foldingRange future in one future which return the list of folding ranges + return semanticTokensPerServerFutures.get(0); //CompletableFutures.mergeInOneFuture(semanticTokensPerServerFutures, cancellationSupport); + }); + } + + private static CompletableFuture getSemanticTokensFor(SemanticTokensParams params, + LanguageServerItem languageServer, + CancellationSupport cancellationSupport) { + return cancellationSupport.execute(languageServer + .getTextDocumentService() + .semanticTokensFull(params), languageServer, LSPRequestConstants.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL) + .thenApplyAsync(semanticTokens -> { + if (semanticTokens == null) { + // textDocument/semanticTokensFull may return null + return null; + } + return new SemanticTokensData(semanticTokens, getLegend(languageServer),languageServer.getSemanticTokensColorsProvider()); + }); + } + + @Nullable + private static SemanticTokensLegend getLegend(LanguageServerItem languageServer) { + var serverCapabilities = languageServer.getServerCapabilities(); + return serverCapabilities.getSemanticTokensProvider() != null ? serverCapabilities.getSemanticTokensProvider().getLegend() : null; + } + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensColorSettingsPage.java b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensColorSettingsPage.java new file mode 100644 index 000000000..a49eee426 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensColorSettingsPage.java @@ -0,0 +1,97 @@ +package com.redhat.devtools.lsp4ij.features.semanticTokens; + +import com.intellij.lang.Language; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.openapi.fileTypes.PlainTextLanguage; +import com.intellij.openapi.fileTypes.SyntaxHighlighter; +import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory; +import com.intellij.openapi.options.colors.AttributesDescriptor; +import com.intellij.openapi.options.colors.ColorDescriptor; +import com.intellij.openapi.options.colors.RainbowColorSettingsPage; +import com.intellij.openapi.util.NlsContexts; +import com.intellij.psi.codeStyle.DisplayPriority; +import com.intellij.psi.codeStyle.DisplayPrioritySortable; +import com.redhat.devtools.lsp4ij.LanguageServerBundle; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.Map; + +public class SemanticTokensColorSettingsPage implements RainbowColorSettingsPage, DisplayPrioritySortable { + + private static final AttributesDescriptor[] ourDescriptors = { + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.class"), SemanticTokensHighlightingColors.CLASS), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.comment"), SemanticTokensHighlightingColors.COMMENT), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.enum"), SemanticTokensHighlightingColors.ENUM), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.enum.member"), SemanticTokensHighlightingColors.ENUM_MEMBER), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.event"), SemanticTokensHighlightingColors.EVENT), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.function.call"), SemanticTokensHighlightingColors.FUNCTION_CALL), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.function.declaration"), SemanticTokensHighlightingColors.FUNCTION_DECLARATION), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.keyword"), SemanticTokensHighlightingColors.KEYWORD), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.macro"), SemanticTokensHighlightingColors.MACRO), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.member"), SemanticTokensHighlightingColors.MEMBER_ATTRIBUTES), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.method.call"), SemanticTokensHighlightingColors.METHOD_CALL), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.method.declaration"), SemanticTokensHighlightingColors.METHOD_DECLARATION), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.modifier"), SemanticTokensHighlightingColors.MODIFIER), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.number"), SemanticTokensHighlightingColors.NUMBER), + // Properties + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.property.static"), SemanticTokensHighlightingColors.STATIC_PROPERTY), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.property.static.readonly"), SemanticTokensHighlightingColors.STATIC_READONLY_PROPERTY), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.property"), SemanticTokensHighlightingColors.READONLY_PROPERTY), + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.property.readonly"), SemanticTokensHighlightingColors.READONLY_PROPERTY), + + new AttributesDescriptor(LanguageServerBundle.message("options.lsp.attribute.descriptor.string"), SemanticTokensHighlightingColors.STRING), + }; + + @Override + public boolean isRainbowType(TextAttributesKey type) { + return false; + } + + @Override + public @Nullable Language getLanguage() { + return null; + } + + @Override + public @Nullable Icon getIcon() { + return null; + } + + @Override + public @NotNull SyntaxHighlighter getHighlighter() { + return SyntaxHighlighterFactory.getSyntaxHighlighter(PlainTextLanguage.INSTANCE, null, null); + } + + @Override + public @NonNls @NotNull String getDemoText() { + return "LSP ..."; + } + + @Override + public @Nullable Map getAdditionalHighlightingTagToDescriptorMap() { + return null; + } + + @Override + public AttributesDescriptor @NotNull [] getAttributeDescriptors() { + return ourDescriptors; + } + + @Override + public ColorDescriptor @NotNull [] getColorDescriptors() { + return ColorDescriptor.EMPTY_ARRAY; + } + + @Override + public @NotNull @NlsContexts.ConfigurableName String getDisplayName() { + return "LSP"; + } + + @Override + public DisplayPriority getPriority() { + return DisplayPriority.LANGUAGE_SETTINGS; + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensColorsProvider.java b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensColorsProvider.java new file mode 100644 index 000000000..4136a6eed --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensColorsProvider.java @@ -0,0 +1,16 @@ +package com.redhat.devtools.lsp4ij.features.semanticTokens; + +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public interface SemanticTokensColorsProvider { + + @Nullable + TextAttributesKey getTextAttributesKey(@NotNull String tokenType, + @NotNull List tokenModifiers, + @NotNull PsiFile file); +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensData.java b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensData.java new file mode 100644 index 000000000..81c34c4f0 --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensData.java @@ -0,0 +1,114 @@ +package com.redhat.devtools.lsp4ij.features.semanticTokens; + +import com.intellij.codeHighlighting.RainbowHighlighter; +import com.intellij.codeInsight.daemon.impl.HighlightInfo; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.colors.TextAttributesKey; +import com.intellij.psi.PsiFile; +import com.redhat.devtools.lsp4ij.LSPIJUtils; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensLegend; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.BitSet; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import static com.intellij.codeHighlighting.RainbowHighlighter.RAINBOW_ELEMENT; + +public class SemanticTokensData { + + private final SemanticTokens semanticTokens; + private final SemanticTokensLegend semanticTokensLegend; + private final SemanticTokensColorsProvider semanticTokensColorsProvider; + + public SemanticTokensData(@NotNull SemanticTokens semanticTokens, + @NotNull SemanticTokensLegend semanticTokensLegend, + @NotNull SemanticTokensColorsProvider semanticTokensColorsProvider) { + this.semanticTokens = semanticTokens; + this.semanticTokensLegend = semanticTokensLegend; + this.semanticTokensColorsProvider = semanticTokensColorsProvider; + } + + @Nullable + public void highlight(@NotNull RainbowHighlighter highlighter, + @NotNull PsiFile file, + @NotNull Document document, + @NotNull Consumer addInfo) { + var dataStream = semanticTokens.getData(); + if (dataStream == null || dataStream.isEmpty()) { + return; + } + + int idx = 0; + int prevLine = 0; + int line = 0; + int offset = 0; + int length = 0; + String tokenType = null; + for (Integer data : dataStream) { + switch (idx % 5) { + case 0: // line + line += data; + break; + case 1: // offset + if (line == prevLine) { + offset += data; + } else { + offset = LSPIJUtils.toOffset(line, data, document); + } + break; + case 2: // length + length = data; + break; + case 3: // token type + tokenType = tokenType(data, semanticTokensLegend.getTokenTypes()); + break; + case 4: // token modifier + prevLine = line; + List tokenModifiers = tokenModifiers(data, semanticTokensLegend.getTokenModifiers()); + int colorIndex = 0;//UsedColors.getOrAddColorIndex((UserDataHolderEx) context, tokenType, highlighter.getColorsCount()); + int start = offset; + int end = offset + length; + TextAttributesKey colorKey = tokenType != null ? semanticTokensColorsProvider.getTextAttributesKey(tokenType, tokenModifiers, file) : null; + if (colorKey != null) { + HighlightInfo highlightInfo = HighlightInfo + .newHighlightInfo(RAINBOW_ELEMENT) + .range(start, end) + .textAttributes(colorKey) + .create(); + //HighlightInfo highlightInfo = highlighter.getInfo(colorIndex, start, end, colorKey); + addInfo.accept(highlightInfo); + } + break; + } + idx++; + } + } + + private List tokenModifiers(Integer data, List legend) { + if (data.intValue() == 0) { + return Collections.emptyList(); + } + final var bitSet = BitSet.valueOf(new long[]{data}); + final var tokenModifiers = new ArrayList(); + for (int i = bitSet.nextSetBit(0); i >= 0; i = bitSet.nextSetBit(i + 1)) { + try { + tokenModifiers.add(legend.get(i)); + } catch (IndexOutOfBoundsException e) { + // no match + } + } + return tokenModifiers; + } + + private String tokenType(Integer index, List tokenTypes) { + if (index == null || index >= tokenTypes.size()) { + return null; + } + return tokenTypes.get(index); + } +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensHighlightingColors.java b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensHighlightingColors.java new file mode 100644 index 000000000..b4a58497f --- /dev/null +++ b/src/main/java/com/redhat/devtools/lsp4ij/features/semanticTokens/SemanticTokensHighlightingColors.java @@ -0,0 +1,43 @@ +package com.redhat.devtools.lsp4ij.features.semanticTokens; + +import com.intellij.openapi.editor.DefaultLanguageHighlighterColors; +import com.intellij.openapi.editor.colors.TextAttributesKey; + +public class SemanticTokensHighlightingColors { + + public static final TextAttributesKey CLASS = TextAttributesKey.createTextAttributesKey("LSP_CLASS", DefaultLanguageHighlighterColors.CLASS_NAME); + public static final TextAttributesKey COMMENT = TextAttributesKey.createTextAttributesKey("LSP_COMMENT", DefaultLanguageHighlighterColors.LINE_COMMENT); + public static final TextAttributesKey DECORATOR = TextAttributesKey.createTextAttributesKey("LSP_DECORATOR", DefaultLanguageHighlighterColors.METADATA); + public static final TextAttributesKey ENUM = TextAttributesKey.createTextAttributesKey("LSP_ENUM", DefaultLanguageHighlighterColors.CLASS_NAME); + public static final TextAttributesKey ENUM_MEMBER = TextAttributesKey.createTextAttributesKey("LSP_ENUM_MEMBER", DefaultLanguageHighlighterColors.IDENTIFIER); + public static final TextAttributesKey EVENT = TextAttributesKey.createTextAttributesKey("LSP_EVENT", DefaultLanguageHighlighterColors.MARKUP_ATTRIBUTE); + + // Function + public static final TextAttributesKey FUNCTION_CALL = TextAttributesKey.createTextAttributesKey("LSP_FUNCTION_CALL", DefaultLanguageHighlighterColors.FUNCTION_CALL); + public static final TextAttributesKey FUNCTION_DECLARATION = TextAttributesKey.createTextAttributesKey("LSP_FUNCTION_DECLARATION", DefaultLanguageHighlighterColors.FUNCTION_DECLARATION); + + public static final TextAttributesKey KEYWORD = TextAttributesKey.createTextAttributesKey("LSP_KEYWORD", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey MACRO = TextAttributesKey.createTextAttributesKey("LSP_MACRO", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey MEMBER_ATTRIBUTES = TextAttributesKey.createTextAttributesKey("LSP_MEMBER_ATTRIBUTES", DefaultLanguageHighlighterColors.FUNCTION_CALL); + + // Property + public static final TextAttributesKey STATIC_PROPERTY = TextAttributesKey.createTextAttributesKey("LSP_STATIC_PROPERTY", DefaultLanguageHighlighterColors.STATIC_FIELD); + public static final TextAttributesKey STATIC_READONLY_PROPERTY = TextAttributesKey.createTextAttributesKey("LSP_STATIC_READONLY_PROPERTY", STATIC_PROPERTY); + public static final TextAttributesKey PROPERTY = TextAttributesKey.createTextAttributesKey("LSP_PROPERTY", DefaultLanguageHighlighterColors.INSTANCE_FIELD); + public static final TextAttributesKey READONLY_PROPERTY = TextAttributesKey.createTextAttributesKey("LSP_READONLY_PROPERTY", PROPERTY); + + // Method + public static final TextAttributesKey METHOD_CALL = TextAttributesKey.createTextAttributesKey("LSP_METHOD_CALL", DefaultLanguageHighlighterColors.FUNCTION_CALL); + public static final TextAttributesKey METHOD_DECLARATION = TextAttributesKey.createTextAttributesKey("LSP_METHOD_DECLARATION", DefaultLanguageHighlighterColors.FUNCTION_DECLARATION); + + public static final TextAttributesKey MODIFIER = TextAttributesKey.createTextAttributesKey("LSP_MODIFIER", DefaultLanguageHighlighterColors.KEYWORD); + public static final TextAttributesKey NUMBER = TextAttributesKey.createTextAttributesKey("LSP_NUMBER", DefaultLanguageHighlighterColors.NUMBER); + + public static final TextAttributesKey STRING = TextAttributesKey.createTextAttributesKey("LSP_STRING", DefaultLanguageHighlighterColors.STRING); + + // Variable + public static final TextAttributesKey CONSTANT = TextAttributesKey.createTextAttributesKey("LSP_CONSTANT", DefaultLanguageHighlighterColors.CONSTANT); + + public static final TextAttributesKey GLOBAL_VARIABLE = TextAttributesKey.createTextAttributesKey("LSP_GLOBAL_VARIABLE", DefaultLanguageHighlighterColors.GLOBAL_VARIABLE); + +} diff --git a/src/main/java/com/redhat/devtools/lsp4ij/internal/ClientCapabilitiesFactory.java b/src/main/java/com/redhat/devtools/lsp4ij/internal/ClientCapabilitiesFactory.java index cd00cd4d8..5c49ddbd2 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/internal/ClientCapabilitiesFactory.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/internal/ClientCapabilitiesFactory.java @@ -203,6 +203,46 @@ public static ClientCapabilities create(Object experimental) { renameCapabilities.setPrepareSupport(true); textDocumentClientCapabilities.setRename(renameCapabilities); + // textDocument/semanticTokens + var semanticTokensCapabilities = new SemanticTokensCapabilities(); + semanticTokensCapabilities.setTokenTypes(List.of("namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator")); + semanticTokensCapabilities.setTokenModifiers(List.of("declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary")); + semanticTokensCapabilities.setFormats(List.of("relative")); + var semanticTokensClientCapabilitiesRequests = new SemanticTokensClientCapabilitiesRequests(true); + semanticTokensCapabilities.setRequests(semanticTokensClientCapabilitiesRequests); + textDocumentClientCapabilities.setSemanticTokens(semanticTokensCapabilities); + // Synchronization support textDocumentClientCapabilities .setSynchronization(new SynchronizationCapabilities(Boolean.TRUE, Boolean.TRUE, Boolean.TRUE)); diff --git a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinition.java b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinition.java index 98f3bc2df..df19cf4c2 100644 --- a/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinition.java +++ b/src/main/java/com/redhat/devtools/lsp4ij/server/definition/LanguageServerDefinition.java @@ -23,6 +23,8 @@ import com.redhat.devtools.lsp4ij.LanguageServerEnablementSupport; import com.redhat.devtools.lsp4ij.LanguageServerFactory; import com.redhat.devtools.lsp4ij.client.LanguageClientImpl; +import com.redhat.devtools.lsp4ij.features.semanticTokens.DefaultSemanticTokensColorsProvider; +import com.redhat.devtools.lsp4ij.features.semanticTokens.SemanticTokensColorsProvider; import org.eclipse.lsp4j.jsonrpc.Launcher; import org.eclipse.lsp4j.services.LanguageServer; import org.jetbrains.annotations.NotNull; @@ -214,4 +216,8 @@ public boolean supportsCurrentEditMode(@NotNull Project project) { public Icon getIcon() { return AllIcons.Webreferences.Server; } + + public SemanticTokensColorsProvider getSemanticTokensColorsProvider() { + return new DefaultSemanticTokensColorsProvider(); + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 97ae01b9c..8ec132048 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -250,6 +250,12 @@ id="LSPFormattingAndRangeBothService" implementation="com.redhat.devtools.lsp4ij.features.formatting.LSPFormattingAndRangeBothService"/> + + + +