Skip to content

Commit

Permalink
feat: Support semantic highlighting (#967)
Browse files Browse the repository at this point in the history
  • Loading branch information
CsCherrYY committed Sep 23, 2021
1 parent 13ade17 commit e4d622d
Show file tree
Hide file tree
Showing 7 changed files with 319 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,6 +71,14 @@ public GradleLanguageServer() {
@Override
public CompletableFuture<InitializeResult> 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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -117,7 +124,7 @@ private Set<PublishDiagnosticsParams> generateDiagnostics(ErrorCollector collect
Map<String, List<Diagnostic>> 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);
Expand All @@ -139,4 +146,16 @@ private Set<PublishDiagnosticsParams> generateDiagnostics(ErrorCollector collect
}
return diagnosticsParams;
}

@Override
public CompletableFuture<SemanticTokens> 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))));
}
}
Original file line number Diff line number Diff line change
@@ -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<URI, List<SemanticToken>> tokens = new HashMap<>();

public List<SemanticToken> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Integer> encodedTokens(List<SemanticToken> tokens) {
tokens.sort(new Comparator<SemanticToken>() {
@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<Integer> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit e4d622d

Please sign in to comment.