From 6243feefffbd4e5ee375e5f8a33a1f1d55525ec7 Mon Sep 17 00:00:00 2001 From: aboyko Date: Mon, 13 May 2024 19:40:09 -0400 Subject: [PATCH] Temporarily bring JDT LS semantic tokens into Boot LS to avoid conflicts --- .../CompositeLanguageServerComponents.java | 56 +- .../tokens/SemanticTokensHandler.java | 18 +- .../semantic/tokens/SemanticTokensUtils.java | 36 + .../util/SimpleTextDocumentService.java | 45 +- .../ide/vscode/commons/util/Collector.java | 6 +- .../ide/vscode/boot/app/JdtConfig.java | 7 +- .../boot/java/JdtSemanticTokensHandler.java | 61 +- .../boot/java/JdtSemanticTokensProvider.java | 4 +- .../JdtDataQuerySemanticTokensProvider.java | 21 +- ...eryPropertiesLanguageServerComponents.java | 2 +- .../data/jpa/queries/JpqlSupportState.java | 2 +- .../QueryPropertiesSemanticTokensHandler.java | 41 +- .../java/reconcilers/CompositeASTVisitor.java | 79 +- .../JavaSemanticTokensProvider.java | 47 ++ .../semantictokens/SemanticTokensVisitor.java | 733 ++++++++++++++++++ .../java/semantictokens/TokenModifier.java | 219 ++++++ .../boot/java/semantictokens/TokenType.java | 134 ++++ ...dtDataQuerySemanticTokensProviderTest.java | 22 +- 18 files changed, 1380 insertions(+), 153 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/JavaSemanticTokensProvider.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/SemanticTokensVisitor.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/TokenModifier.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/TokenType.java diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java index 2a9ce476a..b007ef07e 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/composable/CompositeLanguageServerComponents.java @@ -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> componentsByLanguageId = new HashMap<>(); @@ -219,15 +212,12 @@ public class CompositeLanguageServerComponents implements LanguageServerComponen List semanticTokenHandlers = componentsByLanguageId.values().stream().flatMap(l -> l.stream()).map(c -> c.getSemanticTokensHandler()).filter(o -> o.isPresent()).map(o -> o.get()).collect(Collectors.toList()); List 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 semanticTokensFull(TextDocument doc, CancelChecker cancelChecker) { + return findHandler(doc).map(sth -> sth.semanticTokensFull(doc, cancelChecker)).orElse(SemanticTokensHandler.super.semanticTokensFull(doc, cancelChecker)); } @Override - public Either 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 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 findHandler(TextDocumentIdentifier docId) { - TextDocument doc = server.getTextDocumentService().getLatestSnapshot(docId.getUri()); + private Optional 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(); } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokensHandler.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokensHandler.java index f54c3c19d..594748e96 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokensHandler.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokensHandler.java @@ -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 semanticTokensFull(TextDocument doc, CancelChecker cancelChecker) { return null; } - default Either semanticTokensFullDelta(SemanticTokensDeltaParams params, CancelChecker cancelChecker) { - return null; - } - - default SemanticTokens semanticTokensRange(SemanticTokensRangeParams params, CancelChecker cancelChecker) { + default List semanticTokensRange(TextDocument doc, Range range, CancelChecker cancelChecker) { return null; } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokensUtils.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokensUtils.java index 991693980..ac52dcbb8 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokensUtils.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokensUtils.java @@ -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 mapTokensDataToLsp(TextDocument doc, SemanticTokensLegend legend, + List tokensData) { + + // Sort tokens by start offset + Collections.sort(tokensData); + + List 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; + } } diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java index b538a4b62..242aea824 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/SimpleTextDocumentService.java @@ -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 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> 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 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> computeCodeActions(CancelChecker cancelToken, CodeActionCapabilities capabilities, TextDocument doc, CodeActionParams params) { diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Collector.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Collector.java index 6a17fff08..3f485db8f 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Collector.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/Collector.java @@ -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 implements IRequestor { public List get() { return nodes; } + + public boolean isEmpty() { + return nodes.isEmpty(); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java index f56a4cf6d..f1df8cb78 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/JdtConfig.java @@ -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); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensHandler.java index 4feea31a1..c06a4f624 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensHandler.java @@ -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 tokenProviders; private final SemanticTokensLegend legend; + private JavaSemanticTokensProvider jdtLsProvider; + public JdtSemanticTokensHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, Collection tokenProviders) { this.cuCache = cuCache; this.projectFinder = projectFinder; + ArrayList 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 semanticTokensFull(TextDocument doc, CancelChecker cancelChecker) { + return semanticTokens(doc, cancelChecker, null); + } @Override - public SemanticTokens semanticTokensFull(SemanticTokensParams params, CancelChecker cancelChecker) { - Optional optProject = projectFinder.find(params.getTextDocument()); + public List semanticTokensRange(TextDocument doc, Range range, CancelChecker cancelChecker) { + return semanticTokens(doc, cancelChecker, range); + } + + private List semanticTokens(TextDocument doc, CancelChecker cancelChecker, Range r) { + Optional optProject = projectFinder.find(doc.getId()); if (optProject.isPresent()) { IJavaProject jp = optProject.get(); List 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 applicableTokenProviders, IJavaProject jp, CompilationUnit cu) { + private List computeTokens(List applicableTokenProviders, IJavaProject jp, CompilationUnit cu, Range r) { if (cu == null) { return null; } - List tokensData = applicableTokenProviders.stream().map(tp -> tp.computeTokens(jp, cu)).flatMap(t -> t.stream()).collect(Collectors.toList()); - List toLsp = SemanticTokensUtils.mapTokensDataToLsp(tokensData, legend, offset -> cu.getLineNumber(offset) - 1, cu::getColumnNumber); - return toLsp.isEmpty() ? null : new SemanticTokens(toLsp); + Collector 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(); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensProvider.java index 54217381c..51068f657 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtSemanticTokensProvider.java @@ -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 getTokenTypes(); default List getTokenModifiers() { return Collections.emptyList(); } - List computeTokens(IJavaProject project, CompilationUnit cu); boolean isApplicable(IJavaProject project); + ASTVisitor getTokensComputer(IJavaProject project, CompilationUnit cu, Collector collector); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java index 5421edc8e..65094bcd5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProvider.java @@ -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 computeTokens(IJavaProject jp, CompilationUnit cu) { - List tokensData = new ArrayList<>(); - + public ASTVisitor getTokensComputer(IJavaProject jp, CompilationUnit cu, Collector 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 computeTokensForQueryExpression(SemanticTokensDataProvider provider, Expression valueExp) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpaQueryPropertiesLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpaQueryPropertiesLanguageServerComponents.java index 09108f770..03a3239f3 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpaQueryPropertiesLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpaQueryPropertiesLanguageServerComponents.java @@ -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); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSupportState.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSupportState.java index 6d876f61d..55a089724 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSupportState.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSupportState.java @@ -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()); } })); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryPropertiesSemanticTokensHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryPropertiesSemanticTokensHandler.java index bbc5f6255..d2d365c08 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryPropertiesSemanticTokensHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryPropertiesSemanticTokensHandler.java @@ -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 semanticTokensFull(TextDocument doc, CancelChecker cancelChecker) { if (!supportState.isEnabled()) { - return new SemanticTokens(); + return null; } - Optional optProject = projectFinder.find(params.getTextDocument()); + Optional 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); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/CompositeASTVisitor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/CompositeASTVisitor.java index 904d153d6..f1dc3fece 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/CompositeASTVisitor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/CompositeASTVisitor.java @@ -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 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; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/JavaSemanticTokensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/JavaSemanticTokensProvider.java new file mode 100644 index 000000000..a8caf5786 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/JavaSemanticTokensProvider.java @@ -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 getTokenTypes() { + return Arrays.stream(TokenType.values()).map(TokenType::toString).collect(Collectors.toList()); + } + + @Override + public List 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 collector) { + return new SemanticTokensVisitor(cu, collector); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/SemanticTokensVisitor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/SemanticTokensVisitor.java new file mode 100644 index 000000000..0213fda2a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/SemanticTokensVisitor.java @@ -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 collector; + + public SemanticTokensVisitor(CompilationUnit cu, Collector collector) { + super(true); + this.cu = cu; + this.scanner = createScanner(cu); + this.collector = collector; + } + + private String[] unpackModifiers(int modifiers) { + List 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.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.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. + * + *

+ * NOTE: 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. + *

+ * + * @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: + *

+ * public[ class FooBar extends Foo implements ]Bar + * + *

+ * NOTE: 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. + *

+ * + * @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 { + 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 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 List castNodeList(List nodeList) { + return (List) 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 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 void visitNodeList(List nodeList, NodeVisitor visitor) { + if (nodeList != null) { + for (T node : this.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.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. + * + *

+ * The caller is responsible for making sure that the element type + * of the given list is {@link ASTNode}. + *

+ * + * @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 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 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); + } + } +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/TokenModifier.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/TokenModifier.java new file mode 100644 index 000000000..b81d44cd7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/TokenModifier.java @@ -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; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/TokenType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/TokenType.java new file mode 100644 index 000000000..612e87f12 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/semantictokens/TokenType.java @@ -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; + } + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java index a4036c9db..4e6a77b25 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQuerySemanticTokensProviderTest.java @@ -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 computeTokens(CompilationUnit cu) { + Collector 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 tokens = provider.computeTokens(jp, cu); + List 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 tokens = provider.computeTokens(jp, cu); + List 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 tokens = provider.computeTokens(jp, cu); + List 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 tokens = provider.computeTokens(jp, cu); + List 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 tokens = provider.computeTokens(jp, cu); + List 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 tokens = provider.computeTokens(jp, cu); + List tokens = computeTokens(cu); assertThat(tokens.size()).isZero();