JPQL syntax highlighting via Semantic Tokens

This commit is contained in:
aboyko
2024-04-10 09:57:13 -04:00
parent cf8f50615e
commit 95b8e2f5bd
51 changed files with 16831 additions and 56 deletions

View File

@@ -101,6 +101,11 @@ public class BootJavaConfig implements InitializingBean {
return isAll != null && isAll.booleanValue();
}
public boolean isJpqlEnabled() {
Boolean isEnabled = settings.getBoolean("boot-java", "jpql");
return isEnabled != null && isEnabled.booleanValue();
}
public String[] xmlBeansFoldersToScan() {
String foldersStr = settings.getString("boot-java", "support-spring-xml-config", "scan-folders");
if (foldersStr != null) {

View File

@@ -361,6 +361,9 @@ public class BootLanguageServerBootApp {
String fileName = path.getFileName().toString();
switch (Files.getFileExtension(fileName)) {
case "properties":
if (path.endsWith("/META-INF/jpa-named-queries.properties")) {
return LanguageId.JPA_QUERY_PROPERTIES;
}
return LanguageId.BOOT_PROPERTIES;
case "yml":
return LanguageId.BOOT_PROPERTIES_YAML;

View File

@@ -25,6 +25,9 @@ import org.springframework.context.ApplicationContext;
import org.springframework.ide.vscode.boot.factories.SpringFactoriesLanguageServerComponents;
import org.springframework.ide.vscode.boot.index.cache.IndexCache;
import org.springframework.ide.vscode.boot.java.BootJavaLanguageServerComponents;
import org.springframework.ide.vscode.boot.java.data.jpa.queries.JpaQueryPropertiesLanguageServerComponents;
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.links.JavaElementLocationProvider;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveDataProvider;
@@ -122,7 +125,8 @@ public class BootLanguageServerInitializer implements InitializingBean {
new BootJavaLanguageServerComponents(appContext),
new SpringXMLLanguageServerComponents(server, springIndexer, params, config),
new SpringFactoriesLanguageServerComponents(projectFinder, springIndexer, config),
new PomLanguageServerComponents(server, projectFinder, params.projectObserver, appContext.getBean(SpringProjectsProvider.class))
new PomLanguageServerComponents(server, projectFinder, params.projectObserver, appContext.getBean(SpringProjectsProvider.class)),
new JpaQueryPropertiesLanguageServerComponents(server.getTextDocumentService(), projectFinder, appContext.getBean(JpqlSemanticTokens.class), appContext.getBean(JpqlSupportState.class))
);
for (LanguageServerComponents c : componentsList) {
@@ -152,6 +156,8 @@ public class BootLanguageServerInitializer implements InitializingBean {
components.getDocumentSymbolProvider().ifPresent(documents::onDocumentSymbol);
components.getInlayHintHandler().ifPresent(documents::onInlayHint);
components.getSemanticTokensHandler().ifPresent(documents::onSemanticTokens);
startListeningToPerformReconcile();

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2023 VMware, Inc.
* Copyright (c) 2023, 2024 VMware, 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
@@ -12,6 +12,9 @@ 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.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.reconcilers.AddConfigurationIfBeansPresentReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.AnnotationNodeReconciler;
import org.springframework.ide.vscode.boot.java.reconcilers.AuthorizeHttpRequestsReconciler;
@@ -106,5 +109,9 @@ public class JdtConfig {
@Bean EntityIdForRepoReconciler entityIdForRepoReconciler(SimpleLanguageServer server) {
return new EntityIdForRepoReconciler();
}
@Bean JdtDataQuerySemanticTokensProvider jpqlJdtSemanticTokensProvider(JpqlSemanticTokens provider, JpqlSupportState supportState) {
return new JdtDataQuerySemanticTokensProvider(provider, supportState);
}
}

View File

@@ -0,0 +1,33 @@
/*******************************************************************************
* 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.app;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.commons.languageserver.java.ProjectObserver;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
@Configuration(proxyBeanMethods = false)
public class SpringDataConfig {
@Bean
JpqlSemanticTokens jpqlSemanticTokens() {
return new JpqlSemanticTokens();
}
@Bean
JpqlSupportState jpqlSupportState(SimpleLanguageServer server, ProjectObserver projectObserver, BootJavaConfig config) {
return new JpqlSupportState(server, projectObserver, config);
}
}

View File

@@ -62,6 +62,7 @@ import org.springframework.ide.vscode.commons.languageserver.composable.Language
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver;
import org.springframework.ide.vscode.commons.languageserver.reconcile.IReconcileEngine;
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;
import org.springframework.ide.vscode.commons.languageserver.util.DocumentHighlightHandler;
@@ -111,6 +112,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
private BootJavaReconcileEngine reconcileEngine;
private BootJavaCodeActionProvider codeActionProvider;
private DocumentSymbolHandler docSymbolProvider;
private JdtSemanticTokensHandler semanticTokensHandler;
private SpringProcessTracker liveProcessTracker;
@@ -183,6 +185,11 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
codeActionProvider = appContext.getBean(BootJavaCodeActionProvider.class);
Map<String, JdtSemanticTokensProvider> jdtSemanticTokensProviders = appContext.getBeansOfType(JdtSemanticTokensProvider.class);
if (!jdtSemanticTokensProviders.isEmpty()) {
semanticTokensHandler = new JdtSemanticTokensHandler(cuCache, projectFinder, jdtSemanticTokensProviders.values());
}
config.addListener(ignore -> {
log.info("update live process tracker settings - start");
@@ -353,5 +360,10 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent
return Optional.ofNullable(codeActionProvider);
}
@Override
public Optional<SemanticTokensHandler> getSemanticTokensHandler() {
return Optional.ofNullable(semanticTokensHandler);
}
}

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* 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;
import java.net.URI;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
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.SemanticTokensLegend;
import org.eclipse.lsp4j.SemanticTokensParams;
import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions;
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
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.text.LanguageId;
public class JdtSemanticTokensHandler implements SemanticTokensHandler {
private final CompilationUnitCache cuCache;
private final JavaProjectFinder projectFinder;
private final Collection<JdtSemanticTokensProvider> tokenProviders;
private final SemanticTokensLegend legend;
public JdtSemanticTokensHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, Collection<JdtSemanticTokensProvider> tokenProviders) {
this.cuCache = cuCache;
this.projectFinder = projectFinder;
this.tokenProviders = tokenProviders;
this.legend = new SemanticTokensLegend(
tokenProviders.stream().flatMap(tp -> tp.getTokenTypes().stream()).distinct().collect(Collectors.toList()),
tokenProviders.stream().flatMap(tp -> tp.getTokenModifiers().stream()).distinct().collect(Collectors.toList())
);
}
@Override
public SemanticTokensWithRegistrationOptions getCapability() {
SemanticTokensWithRegistrationOptions capabilities = new SemanticTokensWithRegistrationOptions();
DocumentFilter documentFilter = new DocumentFilter();
documentFilter.setLanguage(LanguageId.JAVA.getId());
capabilities.setDocumentSelector(List.of(documentFilter));
capabilities.setFull(true);
capabilities.setLegend(legend);
return capabilities;
}
@Override
public SemanticTokens semanticTokensFull(SemanticTokensParams params, CancelChecker cancelChecker) {
Optional<IJavaProject> optProject = projectFinder.find(params.getTextDocument());
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, cu));
}
}
return new SemanticTokens();
}
private SemanticTokens computeTokens(List<JdtSemanticTokensProvider> applicableTokenProviders, CompilationUnit cu) {
List<SemanticTokenData> tokensData = applicableTokenProviders.stream().map(tp -> tp.computeTokens(cu)).flatMap(t -> t.stream()).collect(Collectors.toList());
return new SemanticTokens(SemanticTokensUtils.mapTokensDataToLsp(tokensData, legend, offset -> cu.getLineNumber(offset) - 1, cu::getColumnNumber));
}
}

View File

@@ -0,0 +1,27 @@
/*******************************************************************************
* 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;
import java.util.Collections;
import java.util.List;
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;
public interface JdtSemanticTokensProvider {
List<String> getTokenTypes();
default List<String> getTokenModifiers() { return Collections.emptyList(); }
List<SemanticTokenData> computeTokens(CompilationUnit cu);
boolean isApplicable(IJavaProject project);
}

View File

@@ -0,0 +1,140 @@
/*******************************************************************************
* 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.data.jpa.queries;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider;
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;
public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProvider {
private static final String QUERY = "Query";
private static final String FQN_QUERY = "org.springframework.data.jpa.repository." + QUERY;
private final JpqlSemanticTokens provider;
private final JpqlSupportState supportState;
public JdtDataQuerySemanticTokensProvider(JpqlSemanticTokens provider, JpqlSupportState supportState) {
this.provider = provider;
this.supportState = supportState;
}
@Override
public List<String> getTokenTypes() {
return provider.getTokenTypes();
}
@Override
public List<SemanticTokenData> computeTokens(CompilationUnit cu) {
List<SemanticTokenData> tokensData = new ArrayList<>();
cu.accept(new ASTVisitor() {
@Override
public boolean visit(NormalAnnotation a) {
if (isQueryAnnotation(a)) {
Expression queryValueNode = ((List<?>) a.values()).stream()
.filter(MemberValuePair.class::isInstance)
.map(MemberValuePair.class::cast)
.filter(p -> "value".equals(p.getName().getIdentifier()))
.findFirst().map(p -> p.getValue())
.get();
if (queryValueNode instanceof StringLiteral) {
IAnnotationBinding annotationBinding = a.resolveAnnotationBinding();
String query = getJpaQuery(annotationBinding);
if (query != null && !query.isBlank()) {
int valueOffset = queryValueNode.getStartPosition() + 1;
tokensData.addAll(provider.computeTokens(query, valueOffset));
}
}
}
return false;
}
@Override
public boolean visit(SingleMemberAnnotation a) {
if (isQueryAnnotation(a) && a.getValue() instanceof StringLiteral) {
IAnnotationBinding annotationBinding = a.resolveAnnotationBinding();
String query = getJpaQuery(annotationBinding);
if (query != null && !query.isBlank()) {
int valueOffset = a.getValue().getStartPosition() + 1;
tokensData.addAll(provider.computeTokens(query, valueOffset));
}
}
return false;
}
@Override
public boolean visit(MethodInvocation node) {
if ("createQuery".equals(node.getName().getIdentifier()) && node.arguments().size() <= 2 && node.arguments().get(0) instanceof StringLiteral queryExpr) {
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(provider.computeTokens(queryExpr.getLiteralValue(), queryExpr.getStartPosition() + 1));
}
}
}
return super.visit(node);
}
});
return tokensData;
}
private static boolean isQueryAnnotation(Annotation a) {
return FQN_QUERY.equals(a.getTypeName().getFullyQualifiedName())
|| QUERY.equals(a.getTypeName().getFullyQualifiedName());
}
private static String getJpaQuery(IAnnotationBinding annotationBinding) {
if (annotationBinding != null && annotationBinding.getAnnotationType() != null) {
if (FQN_QUERY.equals(annotationBinding.getAnnotationType().getQualifiedName())) {
String query = null;
boolean isNative = false;
for (IMemberValuePairBinding pair : annotationBinding.getAllMemberValuePairs()) {
switch (pair.getName()) {
case "value":
query = (String) pair.getValue();
break;
case "nativeQuery":
isNative = (Boolean) pair.getValue();
break;
default:
}
}
return isNative ? null : query;
}
}
return null;
}
@Override
public boolean isApplicable(IJavaProject project) {
return supportState.isEnabled() && SpringProjectUtil.hasDependencyStartingWith(project, "spring-data-jpa", null);
}
}

View File

@@ -0,0 +1,40 @@
/*******************************************************************************
* 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.data.jpa.queries;
import java.util.Optional;
import java.util.Set;
import org.springframework.ide.vscode.commons.languageserver.composable.LanguageServerComponents;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokensHandler;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
public class JpaQueryPropertiesLanguageServerComponents implements LanguageServerComponents {
private final QueryPropertiesSemanticTokensHandler semanticTokensHandler;
public JpaQueryPropertiesLanguageServerComponents(SimpleTextDocumentService documents, JavaProjectFinder projectsFinder, JpqlSemanticTokens jpqlSemanticTokensProvider, JpqlSupportState supportState) {
this.semanticTokensHandler = new QueryPropertiesSemanticTokensHandler(documents, projectsFinder, jpqlSemanticTokensProvider, supportState);
}
@Override
public Set<LanguageId> getInterestingLanguages() {
return Set.of(LanguageId.JPA_QUERY_PROPERTIES);
}
@Override
public Optional<SemanticTokensHandler> getSemanticTokensHandler() {
return Optional.of(semanticTokensHandler);
}
}

View File

@@ -0,0 +1,147 @@
/*******************************************************************************
* 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.data.jpa.queries;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
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.jpql.antlr.parser.JpqlBaseListener;
import org.springframework.ide.vscode.jpql.antlr.parser.JpqlLexer;
import org.springframework.ide.vscode.jpql.antlr.parser.JpqlParser;
import org.springframework.ide.vscode.jpql.antlr.parser.JpqlParser.Collection_valued_fieldContext;
import org.springframework.ide.vscode.jpql.antlr.parser.JpqlParser.Entity_nameContext;
import org.springframework.ide.vscode.jpql.antlr.parser.JpqlParser.Entity_type_literalContext;
import org.springframework.ide.vscode.jpql.antlr.parser.JpqlParser.Single_valued_object_fieldContext;
import org.springframework.ide.vscode.jpql.antlr.parser.JpqlParser.State_fieldContext;
public class JpqlSemanticTokens implements SemanticTokensDataProvider {
private static List<String> TOKEN_TYPES = List.of("keyword", "type", "class", "string", "number", "operator",
"variable", "method", "modifier", "regexp");
@Override
public List<String> getTokenTypes() {
return TOKEN_TYPES;
}
@Override
public List<SemanticTokenData> computeTokens(String text, int initialOffset) {
List<SemanticTokenData> tokens = new ArrayList<>();
JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(text));
CommonTokenStream antlrTokens = new CommonTokenStream(lexer);
JpqlParser parser = new JpqlParser(antlrTokens);
HashSet<Token> coloredTokens = new HashSet<>();
parser.addParseListener(new JpqlBaseListener() {
private void addToken(Token token, String tokenType) {
tokens.add(new SemanticTokenData(token.getStartIndex() + initialOffset,
token.getStartIndex() + token.getText().length() + initialOffset, tokenType, new String[0]));
coloredTokens.add(token);
}
private void processTerminalNode(TerminalNode node) {
if (coloredTokens.contains(node.getSymbol())) {
return;
}
int type = node.getSymbol().getType();
switch (type) {
case JpqlParser.STRINGLITERAL:
case JpqlParser.CHARACTER:
addToken(node.getSymbol(), "string");
break;
case JpqlParser.LONGLITERAL:
case JpqlParser.INTLITERAL:
case JpqlParser.FLOATLITERAL:
addToken(node.getSymbol(), "number");
break;
case JpqlParser.IDENTIFICATION_VARIABLE:
addToken(node.getSymbol(), "variable");
break;
case JpqlParser.JAVASTRINGLITERAL:
addToken(node.getSymbol(), "class");
break;
case JpqlParser.EQUAL:
case JpqlParser.NOT_EQUAL:
addToken(node.getSymbol(), "operator");
break;
case JpqlParser.WS:
break;
case JpqlParser.SPEL:
addToken(node.getSymbol(), "regexp");
break;
default:
if (JpqlParser.WS < type && type <= JpqlParser.WHERE) {
addToken(node.getSymbol(), "keyword");
} else {
addToken(node.getSymbol(), "modifier");
}
}
}
@Override
public void enterState_field(State_fieldContext ctx) {
if (!coloredTokens.contains(ctx.getStart())) {
addToken(ctx.getStart(), "method");
}
}
@Override
public void enterSingle_valued_object_field(Single_valued_object_fieldContext ctx) {
if (!coloredTokens.contains(ctx.getStart())) {
addToken(ctx.getStart(), "method");
}
}
@Override
public void enterCollection_valued_field(Collection_valued_fieldContext ctx) {
if (!coloredTokens.contains(ctx.getStart())) {
addToken(ctx.getStart(), "method");
}
}
@Override
public void enterEntity_name(Entity_nameContext ctx) {
if (!coloredTokens.contains(ctx.getStart())) {
addToken(ctx.getStart(), "class");
}
}
@Override
public void enterEntity_type_literal(Entity_type_literalContext ctx) {
if (!coloredTokens.contains(ctx.getStart())) {
addToken(ctx.getStart(), "type");
}
}
@Override
public void visitTerminal(TerminalNode node) {
processTerminalNode(node);
}
@Override
public void visitErrorNode(ErrorNode node) {
processTerminalNode(node);
}
});
parser.ql_statement();
return tokens;
}
}

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* 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.data.jpa.queries;
import org.springframework.ide.vscode.boot.app.BootJavaConfig;
import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
public final class JpqlSupportState {
private final SimpleLanguageServer server;
private boolean enabled;
public JpqlSupportState(SimpleLanguageServer server, ProjectObserver projectObserver, BootJavaConfig config) {
this(server, projectObserver, config, config.isJpqlEnabled());
}
public JpqlSupportState(SimpleLanguageServer server, ProjectObserver projectObserver, BootJavaConfig config, boolean enabled) {
this.server = server;
this.enabled = enabled;
config.addListener(v -> setEnabled(config.isJpqlEnabled()));
projectObserver.addListener(ProjectObserver.onAny(jp -> server.getAsync().execute(() -> server.getClient().refreshSemanticTokens())));
}
public synchronized boolean isEnabled() {
return enabled;
}
private synchronized void setEnabled(boolean enabled) {
if (this.enabled != enabled) {
this.enabled = enabled;
server.getAsync().execute(() -> server.getClient().refreshSemanticTokens());
}
}
}

View File

@@ -0,0 +1,109 @@
/*******************************************************************************
* 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.data.jpa.queries;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
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.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;
import org.springframework.ide.vscode.java.properties.parser.ParseResults;
import org.springframework.ide.vscode.java.properties.parser.PropertiesAst;
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 tokensProvider;
private final JpqlSupportState supportState;
public QueryPropertiesSemanticTokensHandler(SimpleTextDocumentService documents, JavaProjectFinder projectFinder, JpqlSemanticTokens jpqlTokensProvider, JpqlSupportState supportState) {
this.documents = documents;
this.projectFinder = projectFinder;
this.tokensProvider = jpqlTokensProvider;
this.supportState = supportState;
}
@Override
public SemanticTokensWithRegistrationOptions getCapability() {
SemanticTokensWithRegistrationOptions capabilities = new SemanticTokensWithRegistrationOptions();
DocumentFilter documentFilter = new DocumentFilter();
documentFilter.setLanguage(LanguageId.JPA_QUERY_PROPERTIES.getId());
capabilities.setDocumentSelector(List.of(documentFilter));
capabilities.setFull(true);
capabilities.setLegend(new SemanticTokensLegend(tokensProvider.getTokenTypes(), tokensProvider.getTypeModifiers()));
return capabilities;
}
@Override
public SemanticTokens semanticTokensFull(SemanticTokensParams params, CancelChecker cancelChecker) {
if (!supportState.isEnabled()) {
return new SemanticTokens();
}
Optional<IJavaProject> optProject = projectFinder.find(params.getTextDocument());
if (optProject.isPresent() && SpringProjectUtil.hasDependencyStartingWith(optProject.get(), "spring-data-jpa", null)) {
TextDocument doc = documents.getLatestSnapshot(params.getTextDocument().getUri());
if (doc != null) {
AntlrParser propertiesParser = new AntlrParser();
ParseResults result = propertiesParser.parse(doc.get());
List<SemanticTokenData> data = new ArrayList<>();
for (PropertiesAst.KeyValuePair node : result.ast.getNodes(PropertiesAst.KeyValuePair.class)) {
Value value = node.getValue();
if (value != null) {
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 SemanticTokensHandler.super.semanticTokensFull(params, cancelChecker);
}
}

View File

@@ -0,0 +1,212 @@
/*******************************************************************************
* 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.data.jpa.queries;
import static org.assertj.core.api.Assertions.assertThat;
import java.nio.file.Paths;
import java.util.List;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.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.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import(HoverTestConf.class)
public class JdtDataQuerySemanticTokensProviderTest {
@Autowired JdtDataQuerySemanticTokensProvider provider;
private ProjectsHarness projects = ProjectsHarness.INSTANCE;
private MavenJavaProject jp;
@BeforeEach
public void setup() throws Exception {
jp = projects.mavenProject("spring-modulith-example-full");
}
@Test
void singleMemberAnnotation() throws Exception {
String source = """
package my.package
import org.springframework.data.jpa.repository.Query;
public interface OwnerRepository {
@Query("SELECT DISTINCT owner FROM Owner owner")
void findByLastName();
}
""";
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(120, 126, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("SELECT");
token = tokens.get(1);
assertThat(token).isEqualTo(new SemanticTokenData(127, 135, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("DISTINCT");
token = tokens.get(2);
assertThat(token).isEqualTo(new SemanticTokenData(136, 141, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
token = tokens.get(3);
assertThat(token).isEqualTo(new SemanticTokenData(142, 146, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("FROM");
token = tokens.get(4);
assertThat(token).isEqualTo(new SemanticTokenData(147, 152, "class", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("Owner");
token = tokens.get(5);
assertThat(token).isEqualTo(new SemanticTokenData(153, 158, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
}
@Test
void normalAnnotation() throws Exception {
String source = """
package my.package
import org.springframework.data.jpa.repository.Query;
public interface OwnerRepository {
@Query(value = "SELECT DISTINCT owner FROM Owner owner")
void findByLastName();
}
""";
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(128, 134, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("SELECT");
token = tokens.get(1);
assertThat(token).isEqualTo(new SemanticTokenData(135, 143, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("DISTINCT");
token = tokens.get(2);
assertThat(token).isEqualTo(new SemanticTokenData(144, 149, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
token = tokens.get(3);
assertThat(token).isEqualTo(new SemanticTokenData(150, 154, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("FROM");
token = tokens.get(4);
assertThat(token).isEqualTo(new SemanticTokenData(155, 160, "class", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("Owner");
token = tokens.get(5);
assertThat(token).isEqualTo(new SemanticTokenData(161, 166, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
}
@Test
void createQueryMethod() throws Exception {
String source = """
package my.package
import jakarta.persistence.EntityManager;
public interface OwnerRepository {
default void findByLastName(EntityManager manager) {
manager.createQuery("SELECT DISTINCT owner FROM Owner owner")
}
}
""";
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(cu);
SemanticTokenData token = tokens.get(0);
assertThat(token).isEqualTo(new SemanticTokenData(176, 182, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("SELECT");
token = tokens.get(1);
assertThat(token).isEqualTo(new SemanticTokenData(183, 191, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("DISTINCT");
token = tokens.get(2);
assertThat(token).isEqualTo(new SemanticTokenData(192, 197, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
token = tokens.get(3);
assertThat(token).isEqualTo(new SemanticTokenData(198, 202, "keyword", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("FROM");
token = tokens.get(4);
assertThat(token).isEqualTo(new SemanticTokenData(203, 208, "class", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("Owner");
token = tokens.get(5);
assertThat(token).isEqualTo(new SemanticTokenData(209, 214, "variable", new String[0]));
assertThat(source.substring(token.start(), token.end())).isEqualTo("owner");
}
@Test
void nativeQuery() throws Exception {
String source = """
package my.package
import org.springframework.data.jpa.repository.Query;
public interface OwnerRepository {
@Query(value = "SELECT DISTINCT owner FROM Owner owner", nativeQuery = true)
void findByLastName();
}
""";
String uri = Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString();
CompilationUnit cu = CompilationUnitCache.parse2(source.toCharArray(), uri, "OwnerRepository.java", jp);
assertThat(cu).isNotNull();
List<SemanticTokenData> tokens = provider.computeTokens(cu);
assertThat(tokens.size()).isZero();
}
}

View File

@@ -0,0 +1,115 @@
/*******************************************************************************
* 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.data.jpa.queries;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData;
public class JpqlSemanticTokensTest {
private JpqlSemanticTokens provider = new JpqlSemanticTokens();
@Test
void simpleQuery_1() {
List<SemanticTokenData> tokens = provider.computeTokens("SELECT owner FROM Owner owner", 0);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 6, "keyword", new String[0]));
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(7, 12, "variable", new String[0]));
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(13, 17, "keyword", new String[0]));
assertThat(tokens.get(3)).isEqualTo(new SemanticTokenData(18, 23, "class", new String[0]));
assertThat(tokens.get(4)).isEqualTo(new SemanticTokenData(24, 29, "variable", new String[0]));
assertThat(tokens.size()).isEqualTo(5);
}
@Test
void initialOffset() {
List<SemanticTokenData> tokens = provider.computeTokens("SELECT owner FROM Owner owner", 3);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(3, 9, "keyword", new String[0]));
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(10, 15, "variable", new String[0]));
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(16, 20, "keyword", new String[0]));
assertThat(tokens.get(3)).isEqualTo(new SemanticTokenData(21, 26, "class", new String[0]));
assertThat(tokens.get(4)).isEqualTo(new SemanticTokenData(27, 32, "variable", new String[0]));
assertThat(tokens.size()).isEqualTo(5);
}
@Test
void query_with_conflicting_groupby() {
List<SemanticTokenData> tokens = provider.computeTokens("SELECT g FROM Group g GROUP BY g.name", 0);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 6, "keyword", new String[0]));
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(7, 8, "variable", new String[0]));
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(9, 13, "keyword", new String[0]));
assertThat(tokens.get(3)).isEqualTo(new SemanticTokenData(14, 19, "class", new String[0]));
assertThat(tokens.get(4)).isEqualTo(new SemanticTokenData(20, 21, "variable", new String[0]));
assertThat(tokens.get(5)).isEqualTo(new SemanticTokenData(22, 27, "keyword", new String[0]));
assertThat(tokens.get(6)).isEqualTo(new SemanticTokenData(28, 30, "keyword", new String[0]));
assertThat(tokens.get(7)).isEqualTo(new SemanticTokenData(31, 32, "variable", new String[0]));
assertThat(tokens.get(8)).isEqualTo(new SemanticTokenData(32, 33, "modifier", new String[0]));
assertThat(tokens.get(9)).isEqualTo(new SemanticTokenData(33, 37, "method", new String[0]));
assertThat(tokens.size()).isEqualTo(10);
}
@Test
void query_with_parameter() {
List<SemanticTokenData> tokens = provider.computeTokens("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName%", 0);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 6, "keyword", new String[0])); // SELECT
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(7, 15, "keyword", new String[0])); // DISTINCT
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(16, 21, "variable", new String[0])); // owner
assertThat(tokens.get(3)).isEqualTo(new SemanticTokenData(22, 26, "keyword", new String[0])); // FROM
assertThat(tokens.get(4)).isEqualTo(new SemanticTokenData(27, 32, "class", new String[0])); // Owner
assertThat(tokens.get(5)).isEqualTo(new SemanticTokenData(33, 38, "variable", new String[0])); // owner
assertThat(tokens.get(6)).isEqualTo(new SemanticTokenData(39, 43, "keyword", new String[0])); // left
assertThat(tokens.get(7)).isEqualTo(new SemanticTokenData(44, 48, "keyword", new String[0])); // join
assertThat(tokens.get(8)).isEqualTo(new SemanticTokenData(50, 55, "variable", new String[0])); // owner
assertThat(tokens.get(9)).isEqualTo(new SemanticTokenData(55, 56, "modifier", new String[0])); // .
assertThat(tokens.get(10)).isEqualTo(new SemanticTokenData(56, 60, "method", new String[0])); // pets
assertThat(tokens.get(11)).isEqualTo(new SemanticTokenData(61, 66, "keyword", new String[0])); // WHERE
assertThat(tokens.get(12)).isEqualTo(new SemanticTokenData(67, 72, "variable", new String[0])); // owner
assertThat(tokens.get(13)).isEqualTo(new SemanticTokenData(72, 73, "modifier", new String[0])); // .
assertThat(tokens.get(14)).isEqualTo(new SemanticTokenData(73, 81, "method", new String[0])); // lastName
assertThat(tokens.get(15)).isEqualTo(new SemanticTokenData(82, 86, "keyword", new String[0])); // LIKE
assertThat(tokens.get(16)).isEqualTo(new SemanticTokenData(87, 88, "modifier", new String[0])); // :
assertThat(tokens.get(17)).isEqualTo(new SemanticTokenData(88, 96, "variable", new String[0])); // lastName
assertThat(tokens.get(18)).isEqualTo(new SemanticTokenData(96, 97, "modifier", new String[0])); // lastName
assertThat(tokens.size()).isEqualTo(19);
}
@Test
void query_with_SPEL() {
List<SemanticTokenData> tokens = provider.computeTokens("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:${id}", 0);
assertThat(tokens.get(0)).isEqualTo(new SemanticTokenData(0, 6, "keyword", new String[0])); // SELECT
assertThat(tokens.get(1)).isEqualTo(new SemanticTokenData(7, 12, "variable", new String[0])); // owner
assertThat(tokens.get(2)).isEqualTo(new SemanticTokenData(13, 17, "keyword", new String[0])); // FROM
assertThat(tokens.get(3)).isEqualTo(new SemanticTokenData(18, 23, "class", new String[0])); // Owner
assertThat(tokens.get(4)).isEqualTo(new SemanticTokenData(24, 29, "variable", new String[0])); // owner
assertThat(tokens.get(5)).isEqualTo(new SemanticTokenData(30, 34, "keyword", new String[0])); // left
assertThat(tokens.get(6)).isEqualTo(new SemanticTokenData(35, 39, "keyword", new String[0])); // join
assertThat(tokens.get(7)).isEqualTo(new SemanticTokenData(40, 45, "keyword", new String[0])); // fetch
assertThat(tokens.get(8)).isEqualTo(new SemanticTokenData(46, 51, "variable", new String[0])); // owner
assertThat(tokens.get(9)).isEqualTo(new SemanticTokenData(51, 52, "modifier", new String[0])); // .
assertThat(tokens.get(10)).isEqualTo(new SemanticTokenData(52, 56, "method", new String[0])); // pets
assertThat(tokens.get(11)).isEqualTo(new SemanticTokenData(57, 62, "keyword", new String[0])); // WHERE
assertThat(tokens.get(12)).isEqualTo(new SemanticTokenData(63, 68, "variable", new String[0])); // owner
assertThat(tokens.get(13)).isEqualTo(new SemanticTokenData(68, 69, "modifier", new String[0])); // .
assertThat(tokens.get(14)).isEqualTo(new SemanticTokenData(69, 71, "method", new String[0])); // id
assertThat(tokens.get(15)).isEqualTo(new SemanticTokenData(72, 73, "operator", new String[0])); // =
assertThat(tokens.get(16)).isEqualTo(new SemanticTokenData(73, 74, "modifier", new String[0])); // :
assertThat(tokens.get(17)).isEqualTo(new SemanticTokenData(74, 79, "regexp", new String[0])); // ${id}
assertThat(tokens.size()).isEqualTo(18);
}
}