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 24, 2024
1 parent 6f183e1 commit 8d5c2b0
Show file tree
Hide file tree
Showing 10 changed files with 507 additions and 2 deletions.
13 changes: 13 additions & 0 deletions src/main/java/com/redhat/devtools/lsp4ij/LSPFileSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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.NotNull;
Expand Down Expand Up @@ -75,6 +76,7 @@ public class LSPFileSupport 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 @@ -94,6 +96,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 @@ -118,6 +121,7 @@ public void dispose() {
getReferenceSupport().cancel();
getDeclarationSupport().cancel();
getTypeDefinitionSupport().cancel();
getSemanticTokensSupport().cancel();
}

/**
Expand Down Expand Up @@ -273,6 +277,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
7 changes: 5 additions & 2 deletions src/main/java/com/redhat/devtools/lsp4ij/LSPIJUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,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 @@ -364,7 +367,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
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
10 changes: 10 additions & 0 deletions src/main/java/com/redhat/devtools/lsp4ij/LanguageServerItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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 org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.Map;

public class LSPRainbowColorSettingsPage implements RainbowColorSettingsPage, DisplayPrioritySortable {
@Override
public boolean isRainbowType(TextAttributesKey type) {
return true;
}

@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<String, TextAttributesKey> getAdditionalHighlightingTagToDescriptorMap() {
return null;
}

@Override
public AttributesDescriptor @NotNull [] getAttributeDescriptors() {
return new AttributesDescriptor[0];
}

@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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 LSPRainbowVisitor extends RainbowVisitor {

private static final Logger LOGGER = LoggerFactory.getLogger(LSPRainbowVisitor.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<SemanticTokensData> 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(), document, info -> super.addInfo(info));
}

}
}

@Override
public @NotNull HighlightVisitor clone() {
return new LSPRainbowVisitor();
}
}
Original file line number Diff line number Diff line change
@@ -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:
*
* <ul>
* <li>LSP 'textDocument/semanticTokens' requests</li>
* </ul>
*/
public class LSPSemanticTokensSupport extends AbstractLSPFeatureSupport<SemanticTokensParams, SemanticTokensData> {

public LSPSemanticTokensSupport(@NotNull PsiFile file) {
super(file);
}

public CompletableFuture<SemanticTokensData> getSemanticTokens(SemanticTokensParams params) {
return super.getFeatureData(params);
}

@Override
protected CompletableFuture<SemanticTokensData> doLoad(SemanticTokensParams params, CancellationSupport cancellationSupport) {
PsiFile file = super.getFile();
return getSemanticTokens(file.getVirtualFile(), file.getProject(), params, cancellationSupport);
}

private static @NotNull CompletableFuture<SemanticTokensData> 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<CompletableFuture<SemanticTokensData>> 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<SemanticTokensData> 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));
});
}

@Nullable
private static SemanticTokensLegend getLegend(LanguageServerItem languageServer) {
var serverCapabilities = languageServer.getServerCapabilities();
return serverCapabilities.getSemanticTokensProvider() != null ? serverCapabilities.getSemanticTokensProvider().getLegend() : null;
}

}
Loading

0 comments on commit 8d5c2b0

Please sign in to comment.