Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support semantic highlighting #967

Merged
merged 3 commits into from
Sep 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it necessary to assign individual modifier for every closure type? How do you assign textmate scopes/styles in downstream themes then?

"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;
}
}