Temporarily bring JDT LS semantic tokens into Boot LS to avoid conflicts

This commit is contained in:
aboyko
2024-05-13 19:40:09 -04:00
parent 92eeec8e65
commit 6243feefff
18 changed files with 1380 additions and 153 deletions

View File

@@ -32,21 +32,16 @@ import org.eclipse.lsp4j.Hover;
import org.eclipse.lsp4j.HoverParams;
import org.eclipse.lsp4j.InlayHint;
import org.eclipse.lsp4j.InlayHintParams;
import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.SemanticTokensDelta;
import org.eclipse.lsp4j.SemanticTokensDeltaParams;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.SemanticTokensRangeParams;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SemanticTokensLegend;
import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.eclipse.lsp4j.WorkspaceSymbol;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensHandler;
import org.springframework.ide.vscode.commons.languageserver.util.CodeActionHandler;
import org.springframework.ide.vscode.commons.languageserver.util.CodeLensHandler;
@@ -65,9 +60,7 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument;
import com.google.common.collect.ImmutableMap;
public class CompositeLanguageServerComponents implements LanguageServerComponents {
private static final Logger log = LoggerFactory.getLogger(CompositeLanguageServerComponents.class);
public static class Builder {
private Map<LanguageId, List<LanguageServerComponents>> componentsByLanguageId = new HashMap<>();
@@ -219,15 +212,12 @@ public class CompositeLanguageServerComponents implements LanguageServerComponen
List<SemanticTokensHandler> semanticTokenHandlers = componentsByLanguageId.values().stream().flatMap(l -> l.stream()).map(c -> c.getSemanticTokensHandler()).filter(o -> o.isPresent()).map(o -> o.get()).collect(Collectors.toList());
List<SemanticTokensWithRegistrationOptions> listCapabilities = semanticTokenHandlers.stream().map(sth -> sth.getCapability()).filter(Objects::nonNull).collect(Collectors.toList());
for (int i = 1; i < listCapabilities.size(); i++) {
SemanticTokensWithRegistrationOptions first = listCapabilities.get(0);
SemanticTokensWithRegistrationOptions current = listCapabilities.get(i);
if (!Objects.equals(first.getFull(), current.getFull())
|| !Objects.equals(first.getLegend(), current.getLegend())
|| !Objects.equals(first.getRange(), current.getRange())) {
throw new IllegalStateException("Incompatible Semantic Token registrations for composite language server components");
}
}
SemanticTokensLegend legend = new SemanticTokensLegend(
listCapabilities.stream().flatMap(cap -> cap.getLegend().getTokenTypes().stream()).distinct().collect(Collectors.toList()),
listCapabilities.stream().flatMap(cap -> cap.getLegend().getTokenModifiers().stream()).distinct().collect(Collectors.toList())
);
this.semanticTokensHanlder = semanticTokenHandlers.isEmpty() ? null : new SemanticTokensHandler() {
@Override
@@ -236,32 +226,24 @@ public class CompositeLanguageServerComponents implements LanguageServerComponen
SemanticTokensWithRegistrationOptions capabilities = new SemanticTokensWithRegistrationOptions();
capabilities.setDocumentSelector(listCapabilities.stream().map(c -> c.getDocumentSelector()).flatMap(l -> l.stream()).collect(Collectors.toList()));
if (!listCapabilities.isEmpty()) {
SemanticTokensWithRegistrationOptions first = listCapabilities.get(0);
capabilities.setFull(first.getFull());
capabilities.setLegend(first.getLegend());
capabilities.setRange(first.getRange());
capabilities.setFull(true);
capabilities.setLegend(legend);
capabilities.setRange(false);
}
return capabilities;
}
@Override
public SemanticTokens semanticTokensFull(SemanticTokensParams params, CancelChecker cancelChecker) {
return findHandler(params.getTextDocument()).map(sth -> sth.semanticTokensFull(params, cancelChecker)).orElse(SemanticTokensHandler.super.semanticTokensFull(params, cancelChecker));
public List<SemanticTokenData> semanticTokensFull(TextDocument doc, CancelChecker cancelChecker) {
return findHandler(doc).map(sth -> sth.semanticTokensFull(doc, cancelChecker)).orElse(SemanticTokensHandler.super.semanticTokensFull(doc, cancelChecker));
}
@Override
public Either<SemanticTokens, SemanticTokensDelta> semanticTokensFullDelta(
SemanticTokensDeltaParams params, CancelChecker cancelChecker) {
return findHandler(params.getTextDocument()).map(sth -> sth.semanticTokensFullDelta(params, cancelChecker)).orElse(SemanticTokensHandler.super.semanticTokensFullDelta(params, cancelChecker));
}
@Override
public SemanticTokens semanticTokensRange(SemanticTokensRangeParams params, CancelChecker cancelChecker) {
return findHandler(params.getTextDocument()).map(sth -> sth.semanticTokensRange(params, cancelChecker)).orElse(SemanticTokensHandler.super.semanticTokensRange(params, cancelChecker));
public List<SemanticTokenData> semanticTokensRange(TextDocument doc, Range range, CancelChecker cancelChecker) {
return findHandler(doc).map(sth -> sth.semanticTokensRange(doc, range, cancelChecker)).orElse(SemanticTokensHandler.super.semanticTokensRange(doc, range, cancelChecker));
}
private Optional<SemanticTokensHandler> findHandler(TextDocumentIdentifier docId) {
TextDocument doc = server.getTextDocumentService().getLatestSnapshot(docId.getUri());
private Optional<SemanticTokensHandler> findHandler(TextDocument doc) {
// Only opened docs ideally should get requests for semantic token to highlight
if (doc != null) {
LanguageId language = doc.getLanguageId();
@@ -269,8 +251,6 @@ public class CompositeLanguageServerComponents implements LanguageServerComponen
if (subComponents != null) {
return subComponents.stream().filter(sc -> sc.getSemanticTokensHandler().isPresent()).map(sc -> sc.getSemanticTokensHandler().get()).findFirst();
}
} else {
log.error("Received Semantic Tokens request for a non opened document: %s".formatted(docId.getUri()));
}
return Optional.empty();
}

View File

@@ -10,28 +10,22 @@
*******************************************************************************/
package org.springframework.ide.vscode.commons.languageserver.semantic.tokens;
import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.SemanticTokensDelta;
import org.eclipse.lsp4j.SemanticTokensDeltaParams;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.SemanticTokensRangeParams;
import java.util.List;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.eclipse.lsp4j.jsonrpc.messages.Either;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
public interface SemanticTokensHandler {
SemanticTokensWithRegistrationOptions getCapability();
default SemanticTokens semanticTokensFull(SemanticTokensParams params, CancelChecker cancelChecker) {
default List<SemanticTokenData> semanticTokensFull(TextDocument doc, CancelChecker cancelChecker) {
return null;
}
default Either<SemanticTokens, SemanticTokensDelta> semanticTokensFullDelta(SemanticTokensDeltaParams params, CancelChecker cancelChecker) {
return null;
}
default SemanticTokens semanticTokensRange(SemanticTokensRangeParams params, CancelChecker cancelChecker) {
default List<SemanticTokenData> semanticTokensRange(TextDocument doc, Range range, CancelChecker cancelChecker) {
return null;
}

View File

@@ -16,9 +16,15 @@ import java.util.List;
import java.util.function.Function;
import org.eclipse.lsp4j.SemanticTokensLegend;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
public class SemanticTokensUtils {
private static Logger log = LoggerFactory.getLogger(SemanticTokensUtils.class);
private static int getSemanticTokenTypeIndex(SemanticTokensLegend legend, String tokenType) {
return legend.getTokenTypes().indexOf(tokenType);
}
@@ -59,5 +65,35 @@ public class SemanticTokensUtils {
return data;
}
public static List<Integer> mapTokensDataToLsp(TextDocument doc, SemanticTokensLegend legend,
List<SemanticTokenData> tokensData) {
// Sort tokens by start offset
Collections.sort(tokensData);
List<Integer> data = new ArrayList<>(tokensData.size() * 5);
// Encode relative positions for tokens
int previousLine = 0;
int previousColumn = 0;
for (SemanticTokenData tokenData : tokensData) {
try {
int currentLine = doc.getLineOfOffset(tokenData.start());
int currentColumn = tokenData.start() - doc.getLineOffset(currentLine);
data.add(currentLine - previousLine);
data.add(currentLine == previousLine ? currentColumn - previousColumn : currentColumn);
data.add(tokenData.end() - tokenData.start());
data.add(SemanticTokensUtils.getSemanticTokenTypeIndex(legend, tokenData.type()));
data.add(SemanticTokensUtils.getSemanticTokenModifiersFlags(legend, tokenData.modifiers()));
previousLine = currentLine;
previousColumn = currentColumn;
} catch (BadLocationException e) {
log.error("", e);
}
}
return data;
}
}

View File

@@ -60,8 +60,6 @@ import org.eclipse.lsp4j.PublishDiagnosticsParams;
import org.eclipse.lsp4j.ReferenceParams;
import org.eclipse.lsp4j.RenameParams;
import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.SemanticTokensDelta;
import org.eclipse.lsp4j.SemanticTokensDeltaParams;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.SemanticTokensRangeParams;
import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions;
@@ -88,6 +86,7 @@ import org.springframework.context.ApplicationContext;
import org.springframework.ide.vscode.commons.languageserver.config.LanguageServerProperties;
import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensHandler;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensUtils;
import org.springframework.ide.vscode.commons.util.Assert;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
@@ -458,30 +457,34 @@ public class SimpleTextDocumentService implements TextDocumentService, DocumentE
@Override
public CompletableFuture<SemanticTokens> semanticTokensFull(SemanticTokensParams params) {
if (semanticTokensHandler == null) {
return CompletableFuture.completedFuture(new SemanticTokens());
} else {
return CompletableFutures.computeAsync(messageWorkerThreadPool, cancelChecker -> semanticTokensHandler.semanticTokensFull(params, cancelChecker));
if (semanticTokensHandler != null) {
TextDocument doc = getLatestSnapshot(params.getTextDocument().getUri());
if (doc != null) {
return CompletableFutures.computeAsync(/*messageWorkerThreadPool,*/ cancelChecker -> semanticTokensHandler.semanticTokensFull(doc, cancelChecker)).thenApply(std -> {
if (std != null && !std.isEmpty()) {
return new SemanticTokens(SemanticTokensUtils.mapTokensDataToLsp(doc, semanticTokensHandler.getCapability().getLegend(), std));
}
return null;
});
}
}
return CompletableFuture.completedFuture(null);
}
@Override
public CompletableFuture<Either<SemanticTokens, SemanticTokensDelta>> semanticTokensFullDelta(
SemanticTokensDeltaParams params) {
if (semanticTokensHandler == null) {
return CompletableFuture.completedFuture(Either.forLeft(new SemanticTokens()));
} else {
return CompletableFutures.computeAsync(messageWorkerThreadPool, cancelChecker -> semanticTokensHandler.semanticTokensFullDelta(params, cancelChecker));
}
}
@Override
public CompletableFuture<SemanticTokens> semanticTokensRange(SemanticTokensRangeParams params) {
if (semanticTokensHandler == null) {
return CompletableFuture.completedFuture(new SemanticTokens());
} else {
return CompletableFutures.computeAsync(messageWorkerThreadPool, cancelChecker -> semanticTokensHandler.semanticTokensRange(params, cancelChecker));
if (semanticTokensHandler != null) {
TextDocument doc = getLatestSnapshot(params.getTextDocument().getUri());
if (doc != null) {
return CompletableFutures.computeAsync(/*messageWorkerThreadPool,*/ cancelChecker -> semanticTokensHandler.semanticTokensRange(doc, params.getRange(), cancelChecker)).thenApply(std -> {
if (std != null && !std.isEmpty()) {
return new SemanticTokens(SemanticTokensUtils.mapTokensDataToLsp(doc, semanticTokensHandler.getCapability().getLegend(), std));
}
return null;
});
}
}
return CompletableFuture.completedFuture(null);
}
private List<Either<Command, CodeAction>> computeCodeActions(CancelChecker cancelToken, CodeActionCapabilities capabilities, TextDocument doc, CodeActionParams params) {

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016-2017 Pivotal, Inc.
* Copyright (c) 2016, 2024 Pivotal, Inc.
* 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
@@ -37,4 +37,8 @@ public class Collector<T> implements IRequestor<T> {
public List<T> get() {
return nodes;
}
public boolean isEmpty() {
return nodes.isEmpty();
}
}

View File

@@ -13,10 +13,10 @@ package org.springframework.ide.vscode.boot.app;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.HqlSemanticTokens;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.QueryJdtAstReconciler;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpqlSemanticTokens;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpqlSupportState;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.QueryJdtAstReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.AddConfigurationIfBeansPresentReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationNodeReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.AuthorizeHttpRequestsReconciler;
@@ -35,6 +35,7 @@ import org.springframework.ide.vscode.boot.java.reconcilers.PreciseBeanTypeRecon
import org.springframework.ide.vscode.boot.java.reconcilers.ServerHttpSecurityLambdaDslReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.UnnecessarySpringExtensionReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.WebSecurityConfigurerAdapterReconciler;
import org.springframework.ide.vscode.boot.java.semantictokens.JavaSemanticTokensProvider;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
@Configuration(proxyBeanMethods = false)
@@ -112,6 +113,10 @@ public class JdtConfig {
return new EntityIdForRepoReconciler();
}
@Bean JavaSemanticTokensProvider javaSemanticTokens() {
return new JavaSemanticTokensProvider();
}
@Bean JdtDataQuerySemanticTokensProvider jpqlJdtSemanticTokensProvider(JpqlSemanticTokens jpqlProvider, HqlSemanticTokens hqlProvider, JpqlSupportState supportState) {
return new JdtDataQuerySemanticTokensProvider(jpqlProvider, hqlProvider, supportState);
}

View File

@@ -11,6 +11,7 @@
package org.springframework.ide.vscode.boot.java;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
@@ -18,18 +19,20 @@ import java.util.stream.Collectors;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.lsp4j.DocumentFilter;
import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.SemanticTokensLegend;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.springframework.ide.vscode.boot.java.reconcilers.CompositeASTVisitor;
import org.springframework.ide.vscode.boot.java.semantictokens.JavaSemanticTokensProvider;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensHandler;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensUtils;
import org.springframework.ide.vscode.commons.util.Collector;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
public class JdtSemanticTokensHandler implements SemanticTokensHandler {
@@ -38,9 +41,19 @@ public class JdtSemanticTokensHandler implements SemanticTokensHandler {
private final Collection<JdtSemanticTokensProvider> tokenProviders;
private final SemanticTokensLegend legend;
private JavaSemanticTokensProvider jdtLsProvider;
public JdtSemanticTokensHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, Collection<JdtSemanticTokensProvider> tokenProviders) {
this.cuCache = cuCache;
this.projectFinder = projectFinder;
ArrayList<JdtSemanticTokensProvider> tokenProvidersList = new ArrayList<>(tokenProviders.size());
for (JdtSemanticTokensProvider tp : tokenProviders) {
if (tp instanceof JavaSemanticTokensProvider jdtLsProvider) {
this.jdtLsProvider = jdtLsProvider;
} else {
tokenProvidersList.add(tp);
}
}
this.tokenProviders = tokenProviders;
this.legend = new SemanticTokensLegend(
tokenProviders.stream().flatMap(tp -> tp.getTokenTypes().stream()).distinct().collect(Collectors.toList()),
@@ -54,31 +67,57 @@ public class JdtSemanticTokensHandler implements SemanticTokensHandler {
DocumentFilter documentFilter = new DocumentFilter();
documentFilter.setLanguage(LanguageId.JAVA.getId());
capabilities.setDocumentSelector(List.of(documentFilter));
capabilities.setFull(true);
capabilities.setFull(false);
capabilities.setRange(true);
capabilities.setLegend(legend);
return capabilities;
}
@Override
public List<SemanticTokenData> semanticTokensFull(TextDocument doc, CancelChecker cancelChecker) {
return semanticTokens(doc, cancelChecker, null);
}
@Override
public SemanticTokens semanticTokensFull(SemanticTokensParams params, CancelChecker cancelChecker) {
Optional<IJavaProject> optProject = projectFinder.find(params.getTextDocument());
public List<SemanticTokenData> semanticTokensRange(TextDocument doc, Range range, CancelChecker cancelChecker) {
return semanticTokens(doc, cancelChecker, range);
}
private List<SemanticTokenData> semanticTokens(TextDocument doc, CancelChecker cancelChecker, Range r) {
Optional<IJavaProject> optProject = projectFinder.find(doc.getId());
if (optProject.isPresent()) {
IJavaProject jp = optProject.get();
List<JdtSemanticTokensProvider> applicableTokenProviders = tokenProviders.stream().filter(tp -> tp.isApplicable(jp)).collect(Collectors.toList());
if (!applicableTokenProviders.isEmpty()) {
return cuCache.withCompilationUnit(jp, URI.create(params.getTextDocument().getUri()), cu -> computeTokens(applicableTokenProviders, jp, cu));
return cuCache.withCompilationUnit(jp, URI.create(doc.getUri()), cu -> computeTokens(applicableTokenProviders, jp, cu, r));
}
}
return null;
}
private SemanticTokens computeTokens(List<JdtSemanticTokensProvider> applicableTokenProviders, IJavaProject jp, CompilationUnit cu) {
private List<SemanticTokenData> computeTokens(List<JdtSemanticTokensProvider> applicableTokenProviders, IJavaProject jp, CompilationUnit cu, Range r) {
if (cu == null) {
return null;
}
List<SemanticTokenData> tokensData = applicableTokenProviders.stream().map(tp -> tp.computeTokens(jp, cu)).flatMap(t -> t.stream()).collect(Collectors.toList());
List<Integer> toLsp = SemanticTokensUtils.mapTokensDataToLsp(tokensData, legend, offset -> cu.getLineNumber(offset) - 1, cu::getColumnNumber);
return toLsp.isEmpty() ? null : new SemanticTokens(toLsp);
Collector<SemanticTokenData> collector = new Collector<>();
CompositeASTVisitor visitor = new CompositeASTVisitor();
applicableTokenProviders.forEach(tp -> visitor.add(tp.getTokensComputer(jp, cu, collector)));
if (r != null) {
if (r.getStart() != null) {
visitor.setStartOffset(cu.getPosition(r.getStart().getLine(), r.getStart().getCharacter()));
}
if (r.getEnd() != null) {
visitor.setEndOffset(cu.getPosition(r.getEnd().getLine(), r.getEnd().getCharacter()));
}
}
cu.accept(visitor);
if (!collector.isEmpty() && jdtLsProvider != null) {
// If there are tokens computed then also run JDT LS tokens provider not to lose JDT LS semantic highlights
cu.accept(jdtLsProvider.getTokensComputer(jp, cu, collector));
}
return collector.get();
}
}

View File

@@ -13,15 +13,17 @@ package org.springframework.ide.vscode.boot.java;
import java.util.Collections;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.util.Collector;
public interface JdtSemanticTokensProvider {
List<String> getTokenTypes();
default List<String> getTokenModifiers() { return Collections.emptyList(); }
List<SemanticTokenData> computeTokens(IJavaProject project, CompilationUnit cu);
boolean isApplicable(IJavaProject project);
ASTVisitor getTokensComputer(IJavaProject project, CompilationUnit cu, Collector<SemanticTokenData> collector);
}

View File

@@ -10,7 +10,6 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@@ -32,6 +31,7 @@ import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensDataProvider;
import org.springframework.ide.vscode.commons.util.Collector;
public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProvider {
@@ -59,13 +59,9 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv
}
@Override
public List<SemanticTokenData> computeTokens(IJavaProject jp, CompilationUnit cu) {
List<SemanticTokenData> tokensData = new ArrayList<>();
public ASTVisitor getTokensComputer(IJavaProject jp, CompilationUnit cu, Collector<SemanticTokenData> tokensData) {
SemanticTokensDataProvider provider = SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? hqlProvider : jpqlProvider;
cu.accept(new ASTVisitor() {
return new ASTVisitor() {
@Override
public boolean visit(NormalAnnotation a) {
if (isQueryAnnotation(a)) {
@@ -100,7 +96,7 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv
if (isNative) {
//TODO: SQL semantic tokens
} else {
tokensData.addAll(computeTokensForQueryExpression(provider, queryExpression));
computeTokensForQueryExpression(provider, queryExpression).forEach(tokensData::accept);
}
}
@@ -112,7 +108,7 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv
@Override
public boolean visit(SingleMemberAnnotation a) {
if (isQueryAnnotation(a)) {
tokensData.addAll(computeTokensForQueryExpression(provider, a.getValue()));
computeTokensForQueryExpression(provider, a.getValue()).forEach(tokensData::accept);
}
return false;
}
@@ -123,16 +119,13 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv
IMethodBinding methodBinding = node.resolveMethodBinding();
if ("jakarta.persistence.EntityManager".equals(methodBinding.getDeclaringClass().getQualifiedName())) {
if (methodBinding.getParameterTypes().length <= 2 && "java.lang.String".equals(methodBinding.getParameterTypes()[0].getQualifiedName())) {
tokensData.addAll(computeTokensForQueryExpression(provider, queryExpr));
computeTokensForQueryExpression(provider, queryExpr).forEach(tokensData::accept);
}
}
}
return super.visit(node);
}
});
return tokensData;
};
}
private static List<SemanticTokenData> computeTokensForQueryExpression(SemanticTokensDataProvider provider, Expression valueExp) {

View File

@@ -27,7 +27,7 @@ public class JpaQueryPropertiesLanguageServerComponents implements LanguageServe
public JpaQueryPropertiesLanguageServerComponents(SimpleTextDocumentService documents, JavaProjectFinder projectsFinder,
JpqlSemanticTokens jpqlSemanticTokensProvider, HqlSemanticTokens hqlSematicTokensProvider, JpqlSupportState supportState) {
this.semanticTokensHandler = new QueryPropertiesSemanticTokensHandler(documents, projectsFinder, jpqlSemanticTokensProvider, hqlSematicTokensProvider, supportState);
this.semanticTokensHandler = new QueryPropertiesSemanticTokensHandler(projectsFinder, jpqlSemanticTokensProvider, hqlSematicTokensProvider, supportState);
this.reconcileEngine = new NamedQueryPropertiesReconcileEngine(projectsFinder);
}

View File

@@ -28,7 +28,7 @@ public final class JpqlSupportState {
this.enabled = enabled;
config.addListener(v -> setEnabled(config.isJpqlEnabled()));
projectObserver.addListener(ProjectObserver.onAny(jp -> {
if (enabled && server.getWorkspaceService().supportsSemanticTokensRefresh()) {
if (this.enabled && server.getWorkspaceService().supportsSemanticTokensRefresh()) {
server.getAsync().execute(() -> server.getClient().refreshSemanticTokens());
}
}));

View File

@@ -11,29 +11,21 @@
package org.springframework.ide.vscode.boot.java.data.jpa.queries;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.lsp4j.DocumentFilter;
import org.eclipse.lsp4j.SemanticTokens;
import org.eclipse.lsp4j.SemanticTokensLegend;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.java.SpringProjectUtil;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensDataProvider;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensHandler;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensUtils;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser;
@@ -43,17 +35,13 @@ import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Value
public class QueryPropertiesSemanticTokensHandler implements SemanticTokensHandler {
private static final Logger log = LoggerFactory.getLogger(QueryPropertiesSemanticTokensHandler.class);
private final JavaProjectFinder projectFinder;
private final SimpleTextDocumentService documents;
private final JpqlSemanticTokens jpqlTokensProvider;
private final HqlSemanticTokens hqlTokensProvider;
private final JpqlSupportState supportState;
public QueryPropertiesSemanticTokensHandler(SimpleTextDocumentService documents, JavaProjectFinder projectFinder, JpqlSemanticTokens jpqlTokensProvider, HqlSemanticTokens hqlTokensProvider, JpqlSupportState supportState) {
this.documents = documents;
public QueryPropertiesSemanticTokensHandler(JavaProjectFinder projectFinder, JpqlSemanticTokens jpqlTokensProvider, HqlSemanticTokens hqlTokensProvider, JpqlSupportState supportState) {
this.projectFinder = projectFinder;
this.jpqlTokensProvider = jpqlTokensProvider;
this.hqlTokensProvider = hqlTokensProvider;
@@ -74,13 +62,12 @@ public class QueryPropertiesSemanticTokensHandler implements SemanticTokensHandl
}
@Override
public SemanticTokens semanticTokensFull(SemanticTokensParams params, CancelChecker cancelChecker) {
public List<SemanticTokenData> semanticTokensFull(TextDocument doc, CancelChecker cancelChecker) {
if (!supportState.isEnabled()) {
return new SemanticTokens();
return null;
}
Optional<IJavaProject> optProject = projectFinder.find(params.getTextDocument());
Optional<IJavaProject> optProject = projectFinder.find(doc.getId());
if (optProject.isPresent() && SpringProjectUtil.hasDependencyStartingWith(optProject.get(), "spring-data-jpa", null)) {
TextDocument doc = documents.getLatestSnapshot(params.getTextDocument().getUri());
if (doc != null) {
SemanticTokensDataProvider tokensProvider = SpringProjectUtil.hasDependencyStartingWith(optProject.get(), "hibernate-core", null) ? hqlTokensProvider : jpqlTokensProvider;
AntlrParser propertiesParser = new AntlrParser();
@@ -92,26 +79,10 @@ public class QueryPropertiesSemanticTokensHandler implements SemanticTokensHandl
data.addAll(tokensProvider.computeTokens(value.decode(), value.getOffset()));
}
}
Collections.sort(data);
SemanticTokensLegend legend = new SemanticTokensLegend(tokensProvider.getTokenTypes(), tokensProvider.getTypeModifiers());
return new SemanticTokens(SemanticTokensUtils.mapTokensDataToLsp(data, legend, t -> {
try {
return doc.getLineOfOffset(t);
} catch (BadLocationException e) {
log.error("", e);
}
return -1;
}, o -> {
try {
return o - doc.getLineOffset(doc.getLineOfOffset(o));
} catch (BadLocationException e) {
log.error("", e);
}
return -1;
}));
return data;
}
}
return SemanticTokensHandler.super.semanticTokensFull(params, cancelChecker);
return SemanticTokensHandler.super.semanticTokensFull(doc, cancelChecker);
}

View File

@@ -13,6 +13,7 @@ package org.springframework.ide.vscode.boot.java.reconcilers;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.FieldDeclaration;
import org.eclipse.jdt.core.dom.ImportDeclaration;
@@ -29,6 +30,8 @@ import org.eclipse.jdt.core.dom.TypeDeclaration;
public class CompositeASTVisitor extends ASTVisitor {
List<ASTVisitor> visitors = new ArrayList<>();
private int startOffset = -1;
private int endOffset = -1;
public void add(ASTVisitor visitor) {
visitors.add(visitor);
@@ -37,8 +40,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(TypeDeclaration node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -46,8 +52,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(MethodInvocation node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -55,8 +64,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(MethodDeclaration node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -71,8 +83,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(FieldDeclaration node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -80,8 +95,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(SingleMemberAnnotation node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -89,8 +107,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(NormalAnnotation node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -98,8 +119,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(MarkerAnnotation node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -107,8 +131,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(ImportDeclaration node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -116,8 +143,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(SimpleType node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -125,8 +155,11 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(QualifiedName node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
@@ -134,10 +167,34 @@ public class CompositeASTVisitor extends ASTVisitor {
@Override
public boolean visit(ReturnStatement node) {
boolean result = true;
if (!checkOffset(node)) {
return false;
}
for (ASTVisitor astVisitor : visitors) {
result &= astVisitor.visit(node);
result |= astVisitor.visit(node);
}
return result;
}
private boolean checkOffset(ASTNode n) {
return (startOffset < 0 || n.getStartPosition() >= startOffset)
|| (endOffset <0 || n.getStartPosition() + n.getLength() < endOffset);
}
public int getStartOffset() {
return startOffset;
}
public void setStartOffset(int startOffset) {
this.startOffset = startOffset;
}
public int getEndOffset() {
return endOffset;
}
public void setEndOffset(int endOffset) {
this.endOffset = endOffset;
}
}

View File

@@ -0,0 +1,47 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, Inc.
* 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.semantictokens;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.util.Collector;
public class JavaSemanticTokensProvider implements JdtSemanticTokensProvider {
@Override
public List<String> getTokenTypes() {
return Arrays.stream(TokenType.values()).map(TokenType::toString).collect(Collectors.toList());
}
@Override
public List<String> getTokenModifiers() {
return Arrays.stream(TokenModifier.values()).map(TokenModifier::toString).collect(Collectors.toList());
}
@Override
public boolean isApplicable(IJavaProject project) {
return true;
}
@Override
public ASTVisitor getTokensComputer(IJavaProject project, CompilationUnit cu,
Collector<SemanticTokenData> collector) {
return new SemanticTokensVisitor(cu, collector);
}
}

View File

@@ -0,0 +1,733 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, Inc.
* 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
// Copied from JDT LS to workaround https://github.com/spring-projects/sts4/issues/1249
package org.springframework.ide.vscode.boot.java.semantictokens;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ITypeRoot;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.ToolFactory;
import org.eclipse.jdt.core.compiler.IScanner;
import org.eclipse.jdt.core.compiler.ITerminalSymbols;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.ClassInstanceCreation;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ExportsDirective;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IPackageBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.Javadoc;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.ModuleDeclaration;
import org.eclipse.jdt.core.dom.ModulePackageAccess;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NameQualifiedType;
import org.eclipse.jdt.core.dom.OpensDirective;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.ParameterizedType;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.QualifiedType;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.RequiresDirective;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.TagElement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeLiteral;
import org.eclipse.jdt.internal.core.dom.util.DOMASTUtil;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.util.Collector;
public class SemanticTokensVisitor extends ASTVisitor {
private CompilationUnit cu;
private final IScanner scanner;
private Collector<SemanticTokenData> collector;
public SemanticTokensVisitor(CompilationUnit cu, Collector<SemanticTokenData> collector) {
super(true);
this.cu = cu;
this.scanner = createScanner(cu);
this.collector = collector;
}
private String[] unpackModifiers(int modifiers) {
List<String> modifierStrings = new ArrayList<>();
for (TokenModifier m : TokenModifier.values()) {
if ((modifiers & m.bitmask) != 0) {
modifierStrings.add(m.toString());
}
}
return modifierStrings.toArray(new String[modifierStrings.size()]);
}
/**
* "Static" modifiers which are always added by {@link #addToken(int, int, TokenType, int)}.
* Modifiers can be set or removed at any time during the visitation process, and as such
* will be applied for all nodes visited during the time a static modifier is set.
*
* Setting and removing the modifiers is done via bitwise OR/AND, see
* {@link TokenModifier#bitmask} and {@link TokenModifier#inverseBitmask}
*/
private int staticModifiers = 0;
/**
* Adds a semantic token to the list of tokens being collected by this
* semantic token visitor.
*
* @param offset The document offset of the semantic token.
* @param length The length of the semantic token.
* @param tokenType The type of the semantic token.
* @param modifiers The bitwise OR of the semantic token modifiers, see {@link TokenModifier#bitmask}.
*
* @apiNote This method is order-dependent because of {@link #encodedTokens()}.
* If semantic tokens are not added in the order they appear in the document,
* the encoding algorithm might discard them.
*/
private void addToken(int offset, int length, TokenType tokenType, int modifiers) {
collector.accept(new SemanticTokenData(offset, offset + length, tokenType.toString(), unpackModifiers(modifiers | staticModifiers)));
}
/**
* Adds a semantic token to the list of tokens being collected by this
* semantic token visitor.
*
* @param node The AST node representing the location of the semantic token.
* @param tokenType The type of the semantic token.
* @param modifiers The bitwise OR of the semantic token modifiers, see {@link TokenModifier#bitmask}.
*
* @apiNote This method is order-dependent because of {@link #encodedTokens()}.
* If semantic tokens are not added in the order they appear in the document,
* the encoding algorithm might discard them.
*/
private void addToken(ASTNode node, TokenType tokenType, int modifiers) {
addToken(node.getStartPosition(), node.getLength(), tokenType, modifiers);
}
/**
* Adds a semantic token to the list of tokens being collected by this
* semantic token visitor.
*
* @param node The AST node representing the location of the semantic token.
* @param tokenType The type of the semantic token.
*
* @apiNote This method is order-dependent because of {@link #encodedTokens()}.
* If semantic tokens are not added in the order they appear in the document,
* the encoding algorithm might discard them.
*/
private void addToken(ASTNode node, TokenType tokenType) {
addToken(node, tokenType, 0);
}
@Override
public boolean visit(TypeLiteral node) {
acceptNode(node.getType());
// Don't add "class" keyword token for recovered type literals
// The AST doesn't always contain the correct information,
// so we need to verfify that the literal is 6 characters longer
// than the type itself (corresponding to the ".class" characters).
// https://github.com/eclipse/eclipse.jdt.ls/issues/1922
if (node.getLength() == node.getType().getLength() + 6) {
// The last 5 characters of a TypeLiteral are the "class" keyword
int offset = node.getStartPosition() + node.getLength() - 5;
addToken(offset, 5, TokenType.KEYWORD, 0);
}
return false;
}
@Override
public boolean visit(Javadoc node) {
staticModifiers |= TokenModifier.DOCUMENTATION.bitmask;
return super.visit(node);
}
@Override
public void endVisit(Javadoc node) {
staticModifiers &= TokenModifier.DOCUMENTATION.inverseBitmask;
super.endVisit(node);
}
@Override
public boolean visit(TagElement node) {
if (node.getTagName() != null) {
// If the token is nested, we need to skip the { character
int offset = node.isNested() ? node.getStartPosition() + 1 : node.getStartPosition();
addToken(offset, node.getTagName().length(), TokenType.KEYWORD, 0);
}
acceptNodeList(node.fragments());
return false;
}
@Override
public boolean visit(PackageDeclaration node) {
acceptNode(node.getJavadoc());
acceptNodeList(node.annotations());
addTokenToSimpleNamesOfName(node.getName(), TokenType.NAMESPACE);
return false;
}
@Override
public boolean visit(ImportDeclaration node) {
staticModifiers |= TokenModifier.IMPORT_DECLARATION.bitmask;
IBinding binding = node.resolveBinding();
if (binding == null || binding instanceof IPackageBinding) {
addTokenToSimpleNamesOfName(node.getName(), TokenType.NAMESPACE);
}
else {
nonPackageNameOfImportDeclarationVisitor(node.getName());
}
staticModifiers &= TokenModifier.IMPORT_DECLARATION.inverseBitmask;
return false;
}
/**
* {@link NodeVisitor} for {@link Name} nodes inside an import declaration that are not package names.
*
* @param nonPackageName
*/
private void nonPackageNameOfImportDeclarationVisitor(Name nonPackageName) {
if (nonPackageName instanceof SimpleName) {
nonPackageName.accept(this);
}
else {
QualifiedName qualifiedName = (QualifiedName) nonPackageName;
Name qualifier = qualifiedName.getQualifier();
if (hasPackageQualifier(qualifiedName)) {
addTokenToSimpleNamesOfName(qualifier, TokenType.NAMESPACE);
}
else {
nonPackageNameOfImportDeclarationVisitor(qualifier);
}
qualifiedName.getName().accept(this);
}
}
/**
* Returns whether a qualified name has a package name as its qualifier,
* even if the qualifier's binding cannot be resolved.
* This is in order to work around an issue where package names in import statements
* cannot have their bindings resolved when their corresponding package is not explicitely
* exported and is part of a module.
*
* @param qualifiedName A qualified name.
* @return {@code true} if the qualified name has a package name as its qualifier,
* {@code false} otherwise.
*/
private boolean hasPackageQualifier(QualifiedName qualifiedName) {
IBinding qualifierBinding = qualifiedName.getQualifier().resolveBinding();
if (qualifierBinding != null) {
return qualifierBinding instanceof IPackageBinding;
}
else {
IBinding binding = qualifiedName.resolveBinding();
return binding instanceof IPackageBinding || binding instanceof ITypeBinding;
}
}
@Override
public boolean visit(Modifier node) {
addToken(node, TokenType.MODIFIER);
return false;
}
@Override
public boolean visit(SimpleName node) {
IBinding binding = node.resolveBinding();
TokenType tokenType = TokenType.getApplicableType(binding);
if (tokenType != null) {
int modifiers
= TokenModifier.checkJavaModifiers(binding)
| TokenModifier.checkConstructor(binding)
| TokenModifier.checkGeneric(binding)
| TokenModifier.checkDeclaration(node);
addToken(node, tokenType, modifiers);
}
return false;
}
@Override
public boolean visit(ModuleDeclaration node) {
acceptJavdocOfModuleDeclaration(node);
acceptNodeList(node.annotations());
addTokenToSimpleNamesOfName(node.getName(), TokenType.NAMESPACE);
acceptNodeList(node.moduleStatements());
return false;
}
/**
* Accepts into the Javadoc of a module declaration.
* This method attempts to work around an issue where {@link ModuleDeclaration#getJavadoc()}
* always returns {@code null}, even if there is an associated Javadoc comment. This is done by
* retrieving the Javadoc from {@link CompilationUnit#getCommentList()} instead.
* This method should be removed once the compiler supports resolving the Javadoc
* for module declarations.
*
* @param moduleDeclaration The module declaration.
*/
private void acceptJavdocOfModuleDeclaration(ModuleDeclaration moduleDeclaration) {
for (Comment comment : this.<Comment>castNodeList(cu.getCommentList())) {
// The start position of the module declaration still includes the Javadoc,
// so we just need to look for a comment with the same start position.
if (comment.getStartPosition() == moduleDeclaration.getStartPosition()) {
comment.accept(this);
break;
}
}
}
@Override
public boolean visit(RequiresDirective node) {
addTokenToSimpleNamesOfName(node.getName(), TokenType.NAMESPACE);
return false;
}
@Override
public boolean visit(ExportsDirective node) {
modulePackageAccessVisitor(node);
return false;
}
@Override
public boolean visit(OpensDirective node) {
modulePackageAccessVisitor(node);
return false;
}
/**
* {@link NodeVisitor} for {@link ModulePackageAccess} nodes.
*
* @param modulePackageAccess
*/
private void modulePackageAccessVisitor(ModulePackageAccess modulePackageAccess) {
acceptNode(modulePackageAccess.getName());
this.<Name>visitNodeList(modulePackageAccess.modules(), module -> {
addTokenToSimpleNamesOfName(module, TokenType.NAMESPACE);
});
}
@Override
public boolean visit(ParameterizedType node) {
acceptNode(node.getType());
visitNodeList(node.typeArguments(), this::typeArgumentVisitor);
return false;
}
@Override
public boolean visit(MethodInvocation node) {
acceptNode(node.getExpression());
visitNodeList(node.typeArguments(), this::typeArgumentVisitor);
acceptNode(node.getName());
acceptNodeList(node.arguments());
return false;
}
/**
* {@link NodeVisitor} for {@link Type} nodes that are type arguments.
*
* @param typeArgument
*/
private void typeArgumentVisitor(Type typeArgument) {
visitSimpleNameOfType(typeArgument, simpleName -> {
IBinding binding = simpleName.resolveBinding();
TokenType tokenType = TokenType.getApplicableType(binding);
if (tokenType != null) {
int modifiers
= TokenModifier.checkJavaModifiers(binding)
| TokenModifier.checkGeneric(binding)
| TokenModifier.TYPE_ARGUMENT.bitmask;
addToken(simpleName, tokenType, modifiers);
}
});
}
@Override
public boolean visit(ClassInstanceCreation node) {
acceptNode(node.getExpression());
visitNodeList(node.typeArguments(), this::typeArgumentVisitor);
visitSimpleNameOfType(node.getType(), simpleName -> {
// Figure out the token type based on the constructed type.
TokenType tokenType = TokenType.getApplicableType(node.resolveTypeBinding());
if (tokenType != null) {
// Figure out the token modifiers based on the constructor method.
// For example, the type could be public, whereas a specific
// constructor may be private.
IMethodBinding constructorBinding = node.resolveConstructorBinding();
int modifiers
= TokenModifier.checkJavaModifiers(constructorBinding)
| TokenModifier.checkGeneric(constructorBinding)
| TokenModifier.CONSTRUCTOR.bitmask;
addToken(simpleName, tokenType, modifiers);
}
});
acceptNodeList(node.arguments());
acceptNode(node.getAnonymousClassDeclaration());
return false;
}
@Override
public boolean visit(RecordDeclaration node) {
acceptNode(node.getJavadoc());
acceptNodeList(node.modifiers());
// Adds token for 'record' keyword. Token type 'MODIFIER' is used to provide the correct highlight colour.
addToken(node.getRestrictedIdentifierStartPosition(), 6, TokenType.MODIFIER, 0);
acceptNode(node.getName());
acceptNodeList(node.typeParameters());
acceptNodeList(node.recordComponents());
acceptNodeList(node.superInterfaceTypes());
acceptNodeList(node.bodyDeclarations());
return false;
}
@Override
public boolean visit(TypeDeclaration node) {
acceptNode(node.getJavadoc());
acceptNodeList(node.modifiers());
tokenizeGapBeforeAndAfterTypeDeclarationName(node, (scannerToken, tokenOffset, tokenLength) -> {
switch (scannerToken) {
case ITerminalSymbols.TokenNameclass:
case ITerminalSymbols.TokenNameinterface:
addToken(tokenOffset, tokenLength, TokenType.MODIFIER, 0);
acceptNode(node.getName());
acceptNodeList(node.typeParameters());
break; // "class" or "interface" keyword tokens
case ITerminalSymbols.TokenNameextends:
addToken(tokenOffset, tokenLength, TokenType.MODIFIER, 0);
acceptNode(node.getSuperclassType());
break; // "extends" keyword token
case ITerminalSymbols.TokenNameimplements:
addToken(tokenOffset, tokenLength, TokenType.MODIFIER, 0);
acceptNodeList(node.superInterfaceTypes());
break; // "implements" keyword token
default:
break; // ignore other tokens
}
});
if (DOMASTUtil.isFeatureSupportedinAST(cu.getAST(), Modifier.SEALED)) {
if (node.getRestrictedIdentifierStartPosition() != -1) {
addToken(node.getRestrictedIdentifierStartPosition(), 7, TokenType.MODIFIER, 0);
}
acceptNodeList(node.permittedTypes());
}
acceptNodeList(node.bodyDeclarations());
return false;
}
/**
* Tries to create an {@link IScanner} for the source of the given compilation unit.
*
* @param cu the compilation unit
* @return the scanner, or {@code null} if not available
*/
private IScanner createScanner(CompilationUnit cu) {
final ITypeRoot typeRoot = cu.getTypeRoot();
if (typeRoot == null) {
return null;
}
final IJavaProject javaProject = typeRoot.getJavaProject();
if (javaProject == null) {
return null;
}
final String source;
try {
source = typeRoot.getSource();
} catch (JavaModelException e) {
return null;
}
if (source == null) {
return null;
}
final String sourceLevel = javaProject.getOption(JavaCore.COMPILER_SOURCE, true);
final String complianceLevel = javaProject.getOption(JavaCore.COMPILER_COMPLIANCE, true);
final boolean enablePreview = JavaCore.ENABLED.equals(javaProject.getOption(JavaCore.COMPILER_PB_ENABLE_PREVIEW_FEATURES, true));
final IScanner scanner = ToolFactory.createScanner(false, false, false, sourceLevel, complianceLevel, enablePreview);
scanner.setSource(source.toCharArray());
return scanner;
}
private int getNextValidToken(IScanner scanner) {
while (true) {
try {
return scanner.getNextToken();
} catch (InvalidInputException e) {
// ignore
}
}
}
/**
* Visitor for {@link IScanner} tokens.
*/
@FunctionalInterface
private interface ScannerTokenVisitor {
/**
* Visits the given scanner token.
*
* @param scannerToken the scanner token ID, see {@link ITerminalSymbols}
* @param tokenOffset the document offset of the scanner token
* @param tokenLength the length of the scanner token
*/
public void visit(int scannerToken, int tokenOffset, int tokenLength);
}
/**
* Uses an {@link IScanner} (if available) to tokenize a source range in the document,
* and visits the scanner tokens in order of occurrence in the source range.
*
* <p>
* <strong>NOTE:</strong> If semantic tokens are added by the visitor, the scan range MUST NOT intersect
* with any other range where semantic tokens can appear, in order to avoid overlapping semantic tokens.
* </p>
*
* @param startPosition the (inclusive) start position of the scan range
* @param endPosition the (exclusive) end position of the scan range
* @param tokenVisitor the visitor to use for scanner tokens
*/
private void tokenizeWithScanner(int startPosition, int endPosition, ScannerTokenVisitor tokenVisitor) {
if (scanner == null) {
return;
}
scanner.resetTo(startPosition, endPosition - 1); // -1 because resetTo wants inclusive endPosition
for (int token = getNextValidToken(scanner); token != ITerminalSymbols.TokenNameEOF; token = getNextValidToken(scanner)) {
int tokenOffset = scanner.getCurrentTokenStartPosition();
int tokenLength = scanner.getCurrentTokenEndPosition() - tokenOffset + 1; // +1 because getCurrentTokenEndPosition is inclusive
tokenVisitor.visit(token, tokenOffset, tokenLength);
}
}
/**
* Uses an {@link IScanner} (if available) to tokenize the gap in the AST just before and after {@link TypeDeclaration#getName()},
* and visits the scanner tokens in order of occurrence in the source range. For example, the following would be tokenized as seen between the square brackets:
* <br><br>
* <code>public[ class FooBar extends Foo implements ]Bar</code>
*
* <p>
* <strong>NOTE:</strong> If semantic tokens are added by the visitor, the scan range MUST NOT intersect
* with any other range where semantic tokens can appear, in order to avoid overlapping semantic tokens.
* </p>
*
* @param typeDeclaration the type declaration node
* @param tokenVisitor the visitor to use for scanner tokens
*/
private void tokenizeGapBeforeAndAfterTypeDeclarationName(TypeDeclaration typeDeclaration, ScannerTokenVisitor tokenVisitor) {
// Try potentially nonexistent start positions, closest first
int gapBeforeNameStart = getEndPosition(typeDeclaration.modifiers());
if (gapBeforeNameStart == -1) {
gapBeforeNameStart = getEndPosition(typeDeclaration.getJavadoc());
}
// Fallback to closest known start position
if (gapBeforeNameStart == -1) {
gapBeforeNameStart = typeDeclaration.getStartPosition();
}
// Try potentially nonexistent end positions, farthest first
int gapBeforeNameEnd = !typeDeclaration.superInterfaceTypes().isEmpty() ? ((ASTNode) (typeDeclaration.superInterfaceTypes().get(0))).getStartPosition() : -1;
if (gapBeforeNameEnd == -1) {
gapBeforeNameEnd = typeDeclaration.getSuperclassType() != null ? typeDeclaration.getSuperclassType().getStartPosition() : -1;
}
if (gapBeforeNameEnd == -1) {
gapBeforeNameEnd = typeDeclaration.getName().getStartPosition();
}
tokenizeWithScanner(gapBeforeNameStart, gapBeforeNameEnd, tokenVisitor);
}
/**
* A node visitor may be used by helpers like {@link #visitSimpleNameOfType(Name, NodeVisitor)},
* and is responsible for the visitation logic of a special kind of node.
*/
@FunctionalInterface
private interface NodeVisitor<T extends ASTNode> {
public void visit(T node);
}
/**
* Helper method to make an unchecked cast from a raw list of AST nodes
* to a generic list of a certain type. The caller is responsible for
* making sure that the cast is safe. An example use case is for
* {@link CompilationUnit#getCommentList()}, which returns a raw list,
* but whose Javadoc states that the element type is {@link Comment}.
*
* @param <T> The type of {@link ASTNode} expected in the node list.
* @param nodeList The node list to be cast.
* @return The cast node list.
*/
@SuppressWarnings("unchecked")
private <T extends ASTNode> List<T> castNodeList(List<?> nodeList) {
return (List<T>) nodeList;
}
/**
* Helper method which visits the nodes in a raw list of AST nodes
* using a {@link NodeVisitor}. An unchecked cast is made via
* {@link #castNodeList(List)}, so the caller is responsible for
* making sure that the cast is safe.
*
* @param <T> The type of {@link ASTNode} expected in the node list.
* @param nodeList The node list to visit (may be {@code null}).
* @param visitor A {@link NodeVisitor}.
*/
private <T extends ASTNode> void visitNodeList(List<?> nodeList, NodeVisitor<T> visitor) {
if (nodeList != null) {
for (T node : this.<T>castNodeList(nodeList)) {
visitor.visit(node);
}
}
}
/**
* Helper method which accepts into the nodes in a raw list of AST nodes.
* An unchecked cast is made via {@link #castNodeList(List)},
* so the caller is responsible for making sure that the cast is safe.
*
* @param nodeList The node list to accept into (may be {@code null}).
*/
private void acceptNodeList(List<?> nodeList) {
if (nodeList != null) {
for (ASTNode node : this.<ASTNode>castNodeList(nodeList)) {
node.accept(this);
}
}
}
/**
* Helper method which performs a null-check on and then accepts into an AST node.
*
* @param node The AST node to accept into (may be {@code null}).
*/
private void acceptNode(ASTNode node) {
if (node != null) {
node.accept(this);
}
}
/**
* Gets the (exclusive) document end position of the given list of AST nodes.
*
* <p>
* The caller is responsible for making sure that the element type
* of the given list is {@link ASTNode}.
* </p>
*
* @param nodeList the list of AST nodes (may be {@code null})
* @return the end position, or {@code -1} if unknown
*/
private int getEndPosition(List<?> nodeList) {
if (nodeList != null && !nodeList.isEmpty()) {
ASTNode lastNode = (ASTNode) nodeList.get(nodeList.size() - 1);
return lastNode.getStartPosition() + lastNode.getLength();
} else {
return -1;
}
}
/**
* Gets the (exclusive) document end position of the given AST node.
*
* @param node the AST node (may be {@code null})
* @return the end position, or {@code -1} if unknown
*/
private int getEndPosition(ASTNode node) {
if (node != null) {
return node.getStartPosition() + node.getLength();
} else {
return -1;
}
}
/**
* Helper method to recursively visit all the simple names of a {@link Name} node
* using the specified {@link NodeVisitor}.
*
* @param name A {@link Name} node (may be {@code null}).
* @param visitor The {@link NodeVisitor} to use.
*/
private void visitSimpleNamesOfName(Name name, NodeVisitor<SimpleName> visitor) {
if (name == null) {
return;
}
if (name instanceof SimpleName simpleName) {
visitor.visit(simpleName);
} else if (name instanceof QualifiedName qualifiedName) {
visitSimpleNamesOfName(qualifiedName.getQualifier(), visitor);
visitor.visit(qualifiedName.getName());
}
}
/**
* Helper method to recursively add a token of the specified type to
* all the simple names of a name.
*
* @param name A {@link Name} node (may be {@code null}).
* @param tokenType The {@link TokenType} to add for all the simple names.
*/
private void addTokenToSimpleNamesOfName(Name name, TokenType tokenType) {
visitSimpleNamesOfName(name, simpleName -> addToken(simpleName, tokenType));
}
/**
* Helper method to find and visit the simple name of a {@code Type} node
* using the specified {@link NodeVisitor}.
*
* @param type A type node (may be {@code null}).
* @param visitor The {@link NodeVisitor} to use.
*/
private void visitSimpleNameOfType(Type type, NodeVisitor<SimpleName> visitor) {
if (type == null) {
return;
}
else if (type instanceof SimpleType simpleType) {
acceptNodeList(simpleType.annotations());
Name simpleTypeName = simpleType.getName();
if (simpleTypeName instanceof SimpleName simpleName) {
visitor.visit(simpleName);
} else if (simpleTypeName instanceof QualifiedName qualifiedName) {
qualifiedName.getQualifier().accept(this);
visitor.visit(qualifiedName.getName());
}
}
else if (type instanceof QualifiedType qualifiedType) {
qualifiedType.getQualifier().accept(this);
acceptNodeList(qualifiedType.annotations());
visitor.visit(qualifiedType.getName());
}
else if (type instanceof NameQualifiedType nameQualifiedType) {
nameQualifiedType.getQualifier().accept(this);
acceptNodeList(nameQualifiedType.annotations());
visitor.visit(nameQualifiedType.getName());
}
else if (type instanceof ParameterizedType parameterizedType) {
visitSimpleNameOfType(parameterizedType.getType(), visitor);
visitNodeList(parameterizedType.typeArguments(), this::typeArgumentVisitor);
}
else {
type.accept(this);
}
}
}

View File

@@ -0,0 +1,219 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, Inc.
* 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
// Copied from JDT LS to workaround https://github.com/spring-projects/sts4/issues/1249
package org.springframework.ide.vscode.boot.java.semantictokens;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.AnnotationTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotationTypeMemberDeclaration;
import org.eclipse.jdt.core.dom.EnumConstantDeclaration;
import org.eclipse.jdt.core.dom.EnumDeclaration;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.RecordDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeParameter;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.lsp4j.SemanticTokenModifiers;
public enum TokenModifier {
// Standard LSP token modifiers, see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens
ABSTRACT(SemanticTokenModifiers.Abstract),
STATIC(SemanticTokenModifiers.Static),
FINAL(SemanticTokenModifiers.Readonly),
DEPRECATED(SemanticTokenModifiers.Deprecated),
DECLARATION(SemanticTokenModifiers.Declaration),
DOCUMENTATION(SemanticTokenModifiers.Documentation),
// Custom token modifiers
PUBLIC("public"),
PRIVATE("private"),
PROTECTED("protected"),
NATIVE("native"),
GENERIC("generic"),
TYPE_ARGUMENT("typeArgument"),
IMPORT_DECLARATION("importDeclaration"),
CONSTRUCTOR("constructor");
/**
* This is the name of the token modifier given to the client, so it
* should be as generic as possible and follow the standard LSP (see below)
* token modifier names where applicable. For example, the generic name of the
* {@link #FINAL} modifier is "readonly", since it has similar meaning.
* Using standardized names makes life easier for theme authors, since
* they don't need to know about language-specific terminology.
*
* @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens
*/
private final String genericName;
/**
* The bitmask for this semantic token modifier.
* Use bitwise OR to combine with other token modifiers.
*/
public final int bitmask = 1 << ordinal();
/**
* The inverse bitmask for this semantic token modifier.
* Use bitwise AND to remove from other token modifiers.
*/
public final int inverseBitmask = ~bitmask;
TokenModifier(String genericName) {
this.genericName = genericName;
}
@Override
public String toString() {
return genericName;
}
/**
* Returns the bitwise OR of all the semantic token modifiers that apply
* based on the binding's {@link Modifier}s and wheter or not it is deprecated.
*
* @param binding A binding.
* @return The bitwise OR of the applicable modifiers for the binding.
*/
public static int checkJavaModifiers(IBinding binding) {
if (binding == null) {
return 0;
}
int modifiers = 0;
int bindingModifiers = binding.getModifiers();
if (Modifier.isPublic(bindingModifiers)) {
modifiers |= PUBLIC.bitmask;
}
if (Modifier.isPrivate(bindingModifiers)) {
modifiers |= PRIVATE.bitmask;
}
if (Modifier.isProtected(bindingModifiers)) {
modifiers |= PROTECTED.bitmask;
}
if (Modifier.isAbstract(bindingModifiers)) {
modifiers |= ABSTRACT.bitmask;
}
if (Modifier.isStatic(bindingModifiers)) {
modifiers |= STATIC.bitmask;
}
if (Modifier.isFinal(bindingModifiers)) {
modifiers |= FINAL.bitmask;
}
if (Modifier.isNative(bindingModifiers)) {
modifiers |= NATIVE.bitmask;
}
if (binding.isDeprecated()) {
modifiers |= DEPRECATED.bitmask;
}
return modifiers;
}
/**
* Checks whether or not a binding represents a constructor.
*
* @param binding A binding.
* @return A bitmask with the {@link #CONSTRUCTOR} bit set accordingly.
*/
public static int checkConstructor(IBinding binding) {
if (binding instanceof IMethodBinding methodBinding && methodBinding.isConstructor()) {
return CONSTRUCTOR.bitmask;
}
return 0;
}
/**
* Checks whether or not a binding represents a generic type or method.
*
* @param binding A binding.
* @return A bitmask with the {@link #GENERIC} bit set accordingly.
*/
public static int checkGeneric(IBinding binding) {
if (binding instanceof ITypeBinding typeBinding) {
if (typeBinding.isGenericType() || typeBinding.isParameterizedType()) {
return GENERIC.bitmask;
}
}
if (binding instanceof IMethodBinding methodBinding) {
if (methodBinding.isGenericMethod() || methodBinding.isParameterizedMethod()) {
return GENERIC.bitmask;
}
return checkGeneric(methodBinding.getDeclaringClass());
}
return 0;
}
/**
* Checks whether or not a binding represents a declaration.
*
* @param binding A binding.
* @return A bitmask with the {@link #DECLARATION} bit set accordingly.
*/
public static int checkDeclaration(SimpleName simpleName) {
if (isDeclaration(simpleName)) {
return DECLARATION.bitmask;
}
return 0;
}
/**
* Returns whether a simple name represents a name that is being defined,
* as opposed to one being referenced.
*
* @param simpleName A simple name.
* @return {@code true} if this node declares a name, and {@code false} otherwise.
*/
private static boolean isDeclaration(SimpleName simpleName) {
StructuralPropertyDescriptor d = simpleName.getLocationInParent();
if (d == null) {
return false;
}
ASTNode parent = simpleName.getParent();
if (parent instanceof TypeDeclaration) {
return (d == TypeDeclaration.NAME_PROPERTY);
}
if (parent instanceof MethodDeclaration) {
return (d == MethodDeclaration.NAME_PROPERTY);
}
if (parent instanceof SingleVariableDeclaration) {
return (d == SingleVariableDeclaration.NAME_PROPERTY);
}
if (parent instanceof VariableDeclarationFragment) {
return (d == VariableDeclarationFragment.NAME_PROPERTY);
}
if (parent instanceof EnumDeclaration) {
return (d == EnumDeclaration.NAME_PROPERTY);
}
if (parent instanceof EnumConstantDeclaration) {
return (d == EnumConstantDeclaration.NAME_PROPERTY);
}
if (parent instanceof TypeParameter) {
return (d == TypeParameter.NAME_PROPERTY);
}
if (parent instanceof AnnotationTypeDeclaration) {
return (d == AnnotationTypeDeclaration.NAME_PROPERTY);
}
if (parent instanceof AnnotationTypeMemberDeclaration) {
return (d == AnnotationTypeMemberDeclaration.NAME_PROPERTY);
}
if (parent instanceof RecordDeclaration) {
return (d == RecordDeclaration.NAME_PROPERTY);
}
return false;
}
}

View File

@@ -0,0 +1,134 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, Inc.
* 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
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
// Copied from JDT LS to workaround https://github.com/spring-projects/sts4/issues/1249
package org.springframework.ide.vscode.boot.java.semantictokens;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.lsp4j.SemanticTokenTypes;
public enum TokenType {
// Standard LSP token types, see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens
NAMESPACE(SemanticTokenTypes.Namespace),
CLASS(SemanticTokenTypes.Class),
INTERFACE(SemanticTokenTypes.Interface),
ENUM(SemanticTokenTypes.Enum),
ENUM_MEMBER(SemanticTokenTypes.EnumMember),
TYPE(SemanticTokenTypes.Type),
TYPE_PARAMETER(SemanticTokenTypes.TypeParameter),
METHOD(SemanticTokenTypes.Method),
PROPERTY(SemanticTokenTypes.Property),
VARIABLE(SemanticTokenTypes.Variable),
PARAMETER(SemanticTokenTypes.Parameter),
MODIFIER(SemanticTokenTypes.Modifier),
KEYWORD(SemanticTokenTypes.Keyword),
// Custom token types
ANNOTATION("annotation"),
ANNOTATION_MEMBER("annotationMember"),
RECORD("record"),
RECORD_COMPONENT("recordComponent");
/**
* This is the name of the token type given to the client, so it
* should be as generic as possible and follow the standard LSP (see below)
* token type names where applicable. For example, the generic name of the
* {@link #PACKAGE} type is "namespace", since it has similar meaning.
* Using standardized names makes life easier for theme authors, since
* they don't need to know about language-specific terminology.
*
* @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_semanticTokens
*/
private String genericName;
TokenType(String genericName) {
this.genericName = genericName;
}
@Override
public String toString() {
return genericName;
}
/**
* Returns the semantic token type that applies to a binding, or
* {@code null} if there is no token type that applies to the binding.
*
* @param binding A binding.
* @return The semantic token type that applies to the binding, or
* {@code null} if there is no token type that applies to the binding.
*/
public static TokenType getApplicableType(IBinding binding) {
if (binding == null) {
return null;
}
switch (binding.getKind()) {
case IBinding.VARIABLE: {
IVariableBinding variableBinding = (IVariableBinding) binding;
if (variableBinding.isEnumConstant()) {
return TokenType.ENUM_MEMBER;
}
if (variableBinding.isRecordComponent()) {
return TokenType.RECORD_COMPONENT;
}
if (variableBinding.isField()) {
return TokenType.PROPERTY;
}
if (variableBinding.isParameter()) {
return TokenType.PARAMETER;
}
return TokenType.VARIABLE;
}
case IBinding.METHOD: {
IMethodBinding methodBinding = (IMethodBinding) binding;
if (methodBinding.isConstructor()) {
return getApplicableType(methodBinding.getDeclaringClass());
}
if (methodBinding.isAnnotationMember()) {
return TokenType.ANNOTATION_MEMBER;
}
return TokenType.METHOD;
}
case IBinding.TYPE: {
ITypeBinding typeBinding = (ITypeBinding) binding;
if (typeBinding.isTypeVariable()) {
return TokenType.TYPE_PARAMETER;
}
if (typeBinding.isAnnotation()) {
return TokenType.ANNOTATION;
}
if (typeBinding.isRecord()) {
return TokenType.RECORD;
}
if (typeBinding.isInterface()) {
return TokenType.INTERFACE;
}
if (typeBinding.isEnum()) {
return TokenType.ENUM;
}
if (typeBinding.isClass()) {
return TokenType.CLASS;
}
return TokenType.TYPE;
}
case IBinding.PACKAGE:
case IBinding.MODULE: {
return TokenType.NAMESPACE;
}
default:
return null;
}
}
}

View File

@@ -23,9 +23,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.bootiful.HoverTestConf;
import org.springframework.ide.vscode.boot.java.reconcilers.CompositeASTVisitor;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject;
import org.springframework.ide.vscode.commons.util.Collector;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@@ -44,6 +46,14 @@ public class JdtDataQuerySemanticTokensProviderTest {
public void setup() throws Exception {
jp = projects.mavenProject("spring-modulith-example-full");
}
private List<SemanticTokenData> computeTokens(CompilationUnit cu) {
Collector<SemanticTokenData> collector = new Collector<>();
CompositeASTVisitor visitor = new CompositeASTVisitor();
visitor.add(provider.getTokensComputer(jp, cu, collector));
cu.accept(visitor);
return collector.get();
}
@Test
void singleMemberAnnotation() throws Exception {
@@ -64,7 +74,7 @@ public class JdtDataQuerySemanticTokensProviderTest {
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(jp, cu);
List<SemanticTokenData> tokens = computeTokens(cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(120, 126, "keyword", new String[0]));
@@ -112,7 +122,7 @@ public class JdtDataQuerySemanticTokensProviderTest {
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(jp, cu);
List<SemanticTokenData> tokens = computeTokens(cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(125, 131, "keyword", new String[0]));
@@ -158,7 +168,7 @@ public class JdtDataQuerySemanticTokensProviderTest {
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(jp, cu);
List<SemanticTokenData> tokens = computeTokens(cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(128, 134, "keyword", new String[0]));
@@ -205,7 +215,7 @@ public class JdtDataQuerySemanticTokensProviderTest {
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(jp, cu);
List<SemanticTokenData> tokens = computeTokens(cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(176, 182, "keyword", new String[0]));
@@ -254,7 +264,7 @@ public class JdtDataQuerySemanticTokensProviderTest {
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(jp, cu);
List<SemanticTokenData> tokens = computeTokens(cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(181, 187, "keyword", new String[0]));
@@ -300,7 +310,7 @@ public class JdtDataQuerySemanticTokensProviderTest {
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(jp, cu);
List<SemanticTokenData> tokens = computeTokens(cu);
assertThat(tokens.size()).isZero();