Skip to content

Commit

Permalink
feat: Support for textDocument/semanticTokens
Browse files Browse the repository at this point in the history
Fixes redhat-developer#238

Signed-off-by: azerr <[email protected]>
  • Loading branch information
angelozerr committed Jun 30, 2024
1 parent c521ff9 commit 3faea86
Show file tree
Hide file tree
Showing 26 changed files with 1,339 additions and 14 deletions.
16 changes: 15 additions & 1 deletion src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
}

Expand All @@ -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();
Expand Down Expand Up @@ -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.
*
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}

/**
Expand Down Expand Up @@ -953,7 +956,7 @@ private static void doApplyEdits(@Nullable Editor editor,
* @param caretOffset the current caret offset and -1 if caret must be not moved.
* @return the increment (positive or negative) used to update caret offset.
*/
private static int applyEdit(int start,
public static int applyEdit(int start,
int end,
@Nullable String newText,
@NotNull Document document,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -498,4 +509,7 @@ private static boolean hasCapability(Boolean capability) {
return capability != null && capability;
}

public SemanticTokensColorsProvider getSemanticTokensColorsProvider() {
return getServerWrapper().getServerDefinition().getSemanticTokensColorsProvider();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -150,6 +151,27 @@ private void refreshInlayHintsForAllOpenedFiles() {
}
}

@Override
public CompletableFuture<Void> 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<Void> createProgress(WorkDoneProgressCreateParams params) {
return progressManager.createProgress(params);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*******************************************************************************/
package com.redhat.devtools.lsp4ij.console.explorer;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.redhat.devtools.lsp4ij.LanguageServerWrapper;
import com.redhat.devtools.lsp4ij.ServerStatus;
Expand Down Expand Up @@ -62,9 +63,12 @@ public void handleLSPMessage(Message message, MessageConsumer messageConsumer, L
return;
}

TracingMessageConsumer tracing = getLSPRequestCacheFor(languageServer);
String log = tracing.log(message, messageConsumer, serverTrace);
invokeLaterIfNeeded(() -> showTrace(processTreeNode, log));
ApplicationManager.getApplication()
.executeOnPooledThread(() -> {
TracingMessageConsumer tracing = getLSPRequestCacheFor(languageServer);
String log = tracing.log(message, messageConsumer, serverTrace);
invokeLaterIfNeeded(() -> showTrace(processTreeNode, log));
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,7 +82,6 @@ public class LSPDocumentationTargetProvider implements DocumentationTargetProvid
// textDocument/hover has been collected correctly
List<Hover> hovers = hoverFuture.getNow(null);
if (hovers != null) {
Editor editor = LSPIJUtils.editorForElement(psiFile);
return hovers
.stream()
.map(hover -> toDocumentTarget(hover, document, psiFile))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*******************************************************************************
* 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.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<String> tokenModifiers,
@NotNull PsiFile file) {
switch (tokenType) {

// class: for identifiers that declare or reference a class type.
case SemanticTokenTypes.Class:
if (hasTokenModifiers(tokenModifiers,
SemanticTokenModifiers.Declaration,
SemanticTokenModifiers.Definition)) {
// with declaration, definition modifiers
return SemanticTokensHighlightingColors.CLASS_DECLARATION;
}
// with other modifiers
return SemanticTokensHighlightingColors.CLASS_REFERENCE;

// 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: for identifiers that declare or reference a namespace, module, or package.
case SemanticTokenTypes.Namespace:
if (hasTokenModifiers(tokenModifiers,
SemanticTokenModifiers.Declaration,
SemanticTokenModifiers.Definition)) {
// with declaration, definition modifiers
return SemanticTokensHighlightingColors.NAMESPACE_DECLARATION;
}
// with other modifiers
return SemanticTokensHighlightingColors.NAMESPACE;

// Number: for tokens that represent a number literal.
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<String> tokenModifiers, String... checkedTokenModifiers) {
if (tokenModifiers.isEmpty()) {
return false;
}
for (var modifier : checkedTokenModifiers) {
if (tokenModifiers.contains(modifier)) {
return true;
}
}
return false;
}

protected boolean hasTokenModifiersOrEmpty(List<String> tokenModifiers, String... checkedTokenModifiers) {
if (tokenModifiers.isEmpty()) {
return true;
}
return hasTokenModifiers(tokenModifiers, checkedTokenModifiers);
}
}
Loading

0 comments on commit 3faea86

Please sign in to comment.