From e4d622d74bafa175784d0f45b59ac85c742bc2b6 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Wed, 1 Sep 2021 12:57:02 +0800 Subject: [PATCH] feat: Support semantic highlighting (#967) --- .../gradle/GradleLanguageServer.java | 18 ++++ .../com/microsoft/gradle/GradleServices.java | 21 +++- .../gradle/compile/SemanticTokenVisitor.java | 101 ++++++++++++++++++ .../gradle/manager/GradleFilesManager.java | 8 ++ .../gradle/semantictokens/SemanticToken.java | 95 ++++++++++++++++ .../gradle/semantictokens/TokenModifier.java | 46 ++++++++ .../gradle/semantictokens/TokenType.java | 31 ++++++ 7 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 gradle-language-server/src/main/java/com/microsoft/gradle/compile/SemanticTokenVisitor.java create mode 100644 gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/SemanticToken.java create mode 100644 gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/TokenModifier.java create mode 100644 gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/TokenType.java diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/GradleLanguageServer.java b/gradle-language-server/src/main/java/com/microsoft/gradle/GradleLanguageServer.java index 84c0f83d1..51d3e2034 100644 --- a/gradle-language-server/src/main/java/com/microsoft/gradle/GradleLanguageServer.java +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/GradleLanguageServer.java @@ -13,11 +13,21 @@ import java.io.IOException; import java.net.Socket; +import java.util.Arrays; +import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import com.microsoft.gradle.semantictokens.TokenModifier; +import com.microsoft.gradle.semantictokens.TokenType; + +import org.eclipse.lsp4j.DocumentFilter; import org.eclipse.lsp4j.InitializeParams; import org.eclipse.lsp4j.InitializeResult; import org.eclipse.lsp4j.SaveOptions; +import org.eclipse.lsp4j.SemanticTokensLegend; +import org.eclipse.lsp4j.SemanticTokensServerFull; +import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; import org.eclipse.lsp4j.ServerCapabilities; import org.eclipse.lsp4j.TextDocumentSyncKind; import org.eclipse.lsp4j.TextDocumentSyncOptions; @@ -61,6 +71,14 @@ public GradleLanguageServer() { @Override public CompletableFuture initialize(InitializeParams params) { ServerCapabilities serverCapabilities = new ServerCapabilities(); + SemanticTokensWithRegistrationOptions semanticOptions = new SemanticTokensWithRegistrationOptions(); + semanticOptions.setFull(new SemanticTokensServerFull(false)); + semanticOptions.setRange(false); + semanticOptions.setDocumentSelector(List.of(new DocumentFilter("gradle", "file", null))); + semanticOptions.setLegend(new SemanticTokensLegend( + Arrays.stream(TokenType.values()).map(TokenType::toString).collect(Collectors.toList()), + Arrays.stream(TokenModifier.values()).map(TokenModifier::toString).collect(Collectors.toList()))); + serverCapabilities.setSemanticTokensProvider(semanticOptions); TextDocumentSyncOptions textDocumentSyncOptions = new TextDocumentSyncOptions(); textDocumentSyncOptions.setOpenClose(Boolean.TRUE); textDocumentSyncOptions.setSave(new SaveOptions(Boolean.TRUE)); diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/GradleServices.java b/gradle-language-server/src/main/java/com/microsoft/gradle/GradleServices.java index bc4308fb6..e86de397d 100644 --- a/gradle-language-server/src/main/java/com/microsoft/gradle/GradleServices.java +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/GradleServices.java @@ -18,9 +18,12 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import com.microsoft.gradle.compile.SemanticTokenVisitor; import com.microsoft.gradle.compile.GradleCompilationUnit; import com.microsoft.gradle.manager.GradleFilesManager; +import com.microsoft.gradle.semantictokens.SemanticToken; import com.microsoft.gradle.utils.LSPUtils; import org.codehaus.groovy.control.CompilationFailedException; @@ -39,6 +42,8 @@ import org.eclipse.lsp4j.DidSaveTextDocumentParams; import org.eclipse.lsp4j.PublishDiagnosticsParams; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SemanticTokens; +import org.eclipse.lsp4j.SemanticTokensParams; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; import org.eclipse.lsp4j.services.TextDocumentService; @@ -48,9 +53,11 @@ public class GradleServices implements TextDocumentService, WorkspaceService, La private LanguageClient client; private GradleFilesManager gradleFilesManager; + private SemanticTokenVisitor semanticTokenVisitor; public GradleServices() { this.gradleFilesManager = new GradleFilesManager(); + this.semanticTokenVisitor = new SemanticTokenVisitor(); } @Override @@ -117,7 +124,7 @@ private Set generateDiagnostics(ErrorCollector collect Map> diagnosticsStorage = new HashMap<>(); for (Message error : collector.getErrors()) { if (error instanceof SyntaxErrorMessage) { - SyntaxException exp = ((SyntaxErrorMessage)error).getCause(); + SyntaxException exp = ((SyntaxErrorMessage) error).getCause(); Range range = LSPUtils.toRange(exp); Diagnostic diagnostic = new Diagnostic(); diagnostic.setRange(range); @@ -139,4 +146,16 @@ private Set generateDiagnostics(ErrorCollector collect } return diagnosticsParams; } + + @Override + public CompletableFuture semanticTokensFull(SemanticTokensParams params) { + URI uri = URI.create(params.getTextDocument().getUri()); + GradleCompilationUnit unit = this.gradleFilesManager.getCompilationUnit(uri); + if (unit == null) { + return CompletableFuture.completedFuture(new SemanticTokens(new ArrayList<>())); + } + this.semanticTokenVisitor.visitCompilationUnit(uri, unit); + return CompletableFuture + .completedFuture(new SemanticTokens(SemanticToken.encodedTokens(this.semanticTokenVisitor.getSemanticTokens(uri)))); + } } diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/compile/SemanticTokenVisitor.java b/gradle-language-server/src/main/java/com/microsoft/gradle/compile/SemanticTokenVisitor.java new file mode 100644 index 000000000..c7ee40a49 --- /dev/null +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/compile/SemanticTokenVisitor.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * Copyright (c) 2021 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ +package com.microsoft.gradle.compile; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.microsoft.gradle.semantictokens.SemanticToken; +import com.microsoft.gradle.semantictokens.TokenModifier; +import com.microsoft.gradle.semantictokens.TokenType; + +import org.codehaus.groovy.ast.ASTNode; +import org.codehaus.groovy.ast.ClassCodeVisitorSupport; +import org.codehaus.groovy.ast.ModuleNode; +import org.codehaus.groovy.ast.expr.MapEntryExpression; +import org.codehaus.groovy.ast.expr.MethodCallExpression; +import org.codehaus.groovy.ast.expr.PropertyExpression; +import org.codehaus.groovy.ast.expr.VariableExpression; +import org.codehaus.groovy.control.SourceUnit; + +public class SemanticTokenVisitor extends ClassCodeVisitorSupport { + + private URI currentUri; + private Map> tokens = new HashMap<>(); + + public List getSemanticTokens(URI uri) { + return this.tokens.get(uri); + } + + private void addToken(int line, int column, int length, TokenType tokenType, int modifiers) { + if (length > 0) { + tokens.get(currentUri).add(new SemanticToken(line, column, length, tokenType, modifiers)); + } + } + + private void addToken(ASTNode node, TokenType tokenType, int modifiers) { + addToken(node.getLineNumber(), node.getColumnNumber(), node.getLength(), tokenType, modifiers); + } + + private void addToken(ASTNode node, TokenType tokenType) { + addToken(node.getLineNumber(), node.getColumnNumber(), node.getLength(), tokenType, 0); + } + + public void visitCompilationUnit(URI uri, GradleCompilationUnit compilationUnit) { + this.currentUri = uri; + compilationUnit.iterator().forEachRemaining(unit -> visitSourceUnit(uri, unit)); + } + + public void visitSourceUnit(URI uri, SourceUnit unit) { + ModuleNode moduleNode = unit.getAST(); + if (moduleNode != null) { + this.tokens.put(uri, new ArrayList<>()); + visitModule(moduleNode); + } + } + + public void visitModule(ModuleNode node) { + node.getClasses().forEach(classNode -> { + super.visitClass(classNode); + }); + } + + @Override + public void visitMethodCallExpression(MethodCallExpression node) { + if (TokenModifier.isDefaultLibrary(node.getMethod().getText())) { + addToken(node.getMethod(), TokenType.FUNCTION, TokenModifier.DEFAULT_LIBRARY.bitmask); + } else { + addToken(node.getMethod(), TokenType.FUNCTION); + } + super.visitMethodCallExpression(node); + } + + @Override + public void visitMapEntryExpression(MapEntryExpression node) { + addToken(node.getKeyExpression(), TokenType.PARAMETER); + super.visitMapEntryExpression(node); + } + + @Override + public void visitVariableExpression(VariableExpression node) { + addToken(node, TokenType.VARIABLE); + super.visitVariableExpression(node); + } + + @Override + public void visitPropertyExpression(PropertyExpression node) { + addToken(node.getProperty(), TokenType.PROPERTY); + super.visitPropertyExpression(node); + } +} diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/manager/GradleFilesManager.java b/gradle-language-server/src/main/java/com/microsoft/gradle/manager/GradleFilesManager.java index 3833c2e48..e033a73a5 100644 --- a/gradle-language-server/src/main/java/com/microsoft/gradle/manager/GradleFilesManager.java +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/manager/GradleFilesManager.java @@ -108,4 +108,12 @@ public GradleCompilationUnit getCompilationUnit(URI uri, Integer version) { return unit; } + public GradleCompilationUnit getCompilationUnit(URI uri) { + // if there is no version info provided, we return the newest version + // when the previous cu exists, otherwise return null + if (this.unitStorage.containsKey(uri)) { + return this.unitStorage.get(uri); + } + return null; + } } diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/SemanticToken.java b/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/SemanticToken.java new file mode 100644 index 000000000..636c566df --- /dev/null +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/SemanticToken.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2021 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Microsoft Corporation - initial API and implementation + *******************************************************************************/ +package com.microsoft.gradle.semantictokens; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class SemanticToken { + private final TokenType tokenType; + private final int tokenModifiers; + private final int line; + private final int column; + private final int length; + + public SemanticToken(int line, int column, int length, TokenType tokenType, int tokenModifiers) { + this.line = line; + this.column = column; + this.length = length; + this.tokenType = tokenType; + this.tokenModifiers = tokenModifiers; + } + + public TokenType getTokenType() { + return tokenType; + } + + public int getTokenModifiers() { + return tokenModifiers; + } + + public int getLine() { + return line; + } + + public int getColumn() { + return column; + } + + public int getLength() { + return length; + } + + // Note: similar logics as JDT.LS, but in groovy AST ranges start from 1 + public static List encodedTokens(List tokens) { + tokens.sort(new Comparator() { + @Override + public int compare(final SemanticToken a, final SemanticToken b) { + int lineResult = Integer.valueOf(a.getLine()).compareTo(Integer.valueOf(b.getLine())); + if (lineResult == 0) { + return Integer.valueOf(a.getColumn()).compareTo(Integer.valueOf(b.getColumn())); + } + return lineResult; + } + }); + int numTokens = tokens.size(); + List data = new ArrayList<>(numTokens * 5); + int currentLine = 0; + int currentColumn = 0; + for (int i = 0; i < numTokens; i++) { + SemanticToken token = tokens.get(i); + int line = token.getLine() - 1; + int column = token.getColumn() - 1; + if (line < 0 || column < 0) { + continue; + } + int deltaLine = line - currentLine; + if (deltaLine != 0) { + currentLine = line; + currentColumn = 0; + } + int deltaColumn = column - currentColumn; + currentColumn = column; + // Disallow duplicate/conflict token (if exists) + if (deltaLine != 0 || deltaColumn != 0 || i == 0) { + int tokenTypeIndex = token.getTokenType().ordinal(); + int tokenModifiers = token.getTokenModifiers(); + data.add(deltaLine); + data.add(deltaColumn); + data.add(token.getLength()); + data.add(tokenTypeIndex); + data.add(tokenModifiers); + } + } + return data; + } +} diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/TokenModifier.java b/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/TokenModifier.java new file mode 100644 index 000000000..95a98ed2e --- /dev/null +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/TokenModifier.java @@ -0,0 +1,46 @@ +/******************************************************************************* + * Copyright (c) 2021 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Microsoft Corporation - initial API and implementation +*******************************************************************************/ +package com.microsoft.gradle.semantictokens; + +import java.util.List; + +import org.eclipse.lsp4j.SemanticTokenModifiers; + +public enum TokenModifier { + + DEFAULT_LIBRARY(SemanticTokenModifiers.DefaultLibrary); + + private String genericName; + // See https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html + private static List defaultLibrary = List.of("afterEvaluate", "allprojects", "ant", "apply", "artifacts", + "beforeEvaluate", "buildscript", "configurations", "configure", "copy", "copySpec", "dependencies", "javaexec", + "repositories", "subprojects", "task"); + + public final int bitmask = 1 << ordinal(); + + TokenModifier(String genericName) { + this.genericName = genericName; + } + + @Override + public String toString() { + return genericName; + } + + public static boolean isDefaultLibrary(String method) { + if (TokenModifier.defaultLibrary.contains(method)) { + return true; + } + return false; + } +} diff --git a/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/TokenType.java b/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/TokenType.java new file mode 100644 index 000000000..f3c7d1398 --- /dev/null +++ b/gradle-language-server/src/main/java/com/microsoft/gradle/semantictokens/TokenType.java @@ -0,0 +1,31 @@ +/******************************************************************************* + * Copyright (c) 2021 Microsoft Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Microsoft Corporation - initial API and implementation +*******************************************************************************/ +package com.microsoft.gradle.semantictokens; + +import org.eclipse.lsp4j.SemanticTokenTypes; + +public enum TokenType { + FUNCTION(SemanticTokenTypes.Function), PROPERTY(SemanticTokenTypes.Property), VARIABLE(SemanticTokenTypes.Variable), + PARAMETER(SemanticTokenTypes.Parameter); + + private String genericName; + + TokenType(String genericName) { + this.genericName = genericName; + } + + @Override + public String toString() { + return genericName; + } +}