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 b007ef07e..c31152b45 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 @@ -31,7 +31,6 @@ import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; import org.eclipse.lsp4j.InlayHint; -import org.eclipse.lsp4j.InlayHintParams; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.SemanticTokensLegend; import org.eclipse.lsp4j.SemanticTokensWithRegistrationOptions; @@ -175,15 +174,14 @@ public class CompositeLanguageServerComponents implements LanguageServerComponen this.inlayHintHandler = new InlayHintHandler() { @Override - public List handle(CancelChecker token, InlayHintParams params) { - TextDocument doc = server.getTextDocumentService().getLatestSnapshot(params.getTextDocument().getUri()); + public List handle(TextDocument doc, Range r, CancelChecker token) { LanguageId language = doc.getLanguageId(); List subComponents = componentsByLanguageId.get(language); if (subComponents != null) { return subComponents.stream() .map(sc -> sc.getInlayHintHandler()) .filter(h -> h.isPresent()) - .flatMap(h -> h.get().handle(token, params).stream()) + .flatMap(h -> h.get().handle(doc, r, token).stream()) .collect(Collectors.toList()); } // No applicable subEngine... diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokenData.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokenData.java index 8c3b84efa..834745dec 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokenData.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/semantic/tokens/SemanticTokenData.java @@ -13,7 +13,12 @@ package org.springframework.ide.vscode.commons.languageserver.semantic.tokens; import java.util.Arrays; import java.util.Objects; -public record SemanticTokenData(int start, int end, String type, String[] modifiers) implements Comparable{ +public record SemanticTokenData( + int start, + int end, + String type, + String[] modifiers + ) implements Comparable { @Override public int compareTo(SemanticTokenData o) { diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/InlayHintHandler.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/InlayHintHandler.java index 6c9c09f06..c3d89d5c6 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/InlayHintHandler.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/util/InlayHintHandler.java @@ -13,11 +13,12 @@ package org.springframework.ide.vscode.commons.languageserver.util; import java.util.List; import org.eclipse.lsp4j.InlayHint; -import org.eclipse.lsp4j.InlayHintParams; +import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.commons.util.text.TextDocument; public interface InlayHintHandler { - List handle(CancelChecker token, InlayHintParams params); + List handle(TextDocument doc, Range range, CancelChecker cancelChecker); } 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 13f145af2..60c2dfae2 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 @@ -581,9 +581,12 @@ public class SimpleTextDocumentService implements TextDocumentService, DocumentE InlayHintHandler handler = this.inlayHintHandler; if (handler != null) { - return CompletableFutures.computeAsync(messageWorkerThreadPool, cancelToken -> { - return handler.handle(cancelToken, params); - }); + TextDocument doc = getLatestSnapshot(params.getTextDocument().getUri()); + if (doc != null) { + return CompletableFutures.computeAsync(messageWorkerThreadPool, cancelToken -> { + return handler.handle(doc, params.getRange(), cancelToken); + }); + } } return CompletableFuture.completedFuture(Collections.emptyList()); } diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 513ef6cc3..24b46c052 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -41,6 +41,7 @@ import org.eclipse.lsp4j.CompletionItemTag; import org.eclipse.lsp4j.CompletionList; import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DocumentHighlight; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.InsertReplaceEdit; @@ -240,6 +241,15 @@ public class Editor { assertEquals(ImmutableList.copyOf(expectedHighlights), actualHighlights); return ranges; } + + public void assertDocumentHighlights(String afterString, DocumentHighlight... expected) throws Exception { + int pos = getRawText().indexOf(afterString); + if (pos>=0) { + pos += afterString.length(); + } + List actual = harness.getDocumentHighlights(doc.getId(), doc.toPosition(pos)); + assertEquals(ImmutableList.copyOf(expected), actual); + } /** * Get the editor text, with cursor markers inserted (for easy textual comparison diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index 0d05db79a..f0e2de13d 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -35,6 +35,7 @@ import java.util.Map.Entry; import java.util.Random; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.function.Function; import java.util.stream.Collectors; @@ -65,6 +66,8 @@ import org.eclipse.lsp4j.DidChangeTextDocumentParams; import org.eclipse.lsp4j.DidChangeWatchedFilesParams; import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; +import org.eclipse.lsp4j.DocumentHighlight; +import org.eclipse.lsp4j.DocumentHighlightParams; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolCapabilities; import org.eclipse.lsp4j.DocumentSymbolParams; @@ -679,6 +682,9 @@ public class LanguageServerHarness { return getServer().getTextDocumentService().codeLens(params).get(); } + public List getDocumentHighlights(TextDocumentIdentifier docId, Position cursor) throws InterruptedException, ExecutionException { + return getServer().getTextDocumentService().documentHighlight(new DocumentHighlightParams(docId, cursor)).get(); + } public CompletionItem resolveCompletionItem(CompletionItem maybeUnresolved) { if (getServer().hasLazyCompletionResolver()) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java index c265a96b4..8bd40d6e2 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootLanguageServerBootApp.java @@ -48,6 +48,8 @@ import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler; import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.QualifierDefinitionProvider; import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaCodeActionProvider; import org.springframework.ide.vscode.boot.java.handlers.BootJavaReconcileEngine; import org.springframework.ide.vscode.boot.java.handlers.JavaCodeActionHandler; @@ -394,12 +396,13 @@ public class BootLanguageServerBootApp { } @Bean - JavaDefinitionHandler javaDefinitionHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) { + JavaDefinitionHandler javaDefinitionHandler(SimpleLanguageServer server, CompilationUnitCache cuCache, JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex, JdtDataQuerySemanticTokensProvider qurySemanticTokens) { return new JavaDefinitionHandler(cuCache, projectFinder, List.of( new ValueDefinitionProvider(), new DependsOnDefinitionProvider(springIndex), new ResourceDefinitionProvider(springIndex), - new QualifierDefinitionProvider(springIndex))); + new QualifierDefinitionProvider(springIndex), + new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens))); } @Bean 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 f7e3cf703..e4864e007 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 @@ -20,7 +20,9 @@ import org.springframework.ide.vscode.boot.java.cron.CronSemanticTokens; import org.springframework.ide.vscode.boot.java.cron.JdtCronReconciler; import org.springframework.ide.vscode.boot.java.cron.JdtCronSemanticTokensProvider; import org.springframework.ide.vscode.boot.java.data.jpa.queries.HqlSemanticTokens; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQueriesInlayHintsProvider; import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryDocHighlightsProvider; 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; @@ -130,6 +132,14 @@ public class JdtConfig { return new JdtDataQuerySemanticTokensProvider(jpqlProvider, hqlProvider, supportState, spelSemanticTokens); } + @Bean JdtDataQueriesInlayHintsProvider jdtDataQueriesInlayHintsProvider(JdtDataQuerySemanticTokensProvider semanticTokensProvider) { + return new JdtDataQueriesInlayHintsProvider(semanticTokensProvider); + } + + @Bean JdtQueryDocHighlightsProvider jdtDocHighlightsProvider(JdtDataQuerySemanticTokensProvider semanticTokensProvider) { + return new JdtQueryDocHighlightsProvider(semanticTokensProvider); + } + @Bean JdtCronSemanticTokensProvider jdtCronSemanticTokensProvider(CronSemanticTokens cronProvider) { return new JdtCronSemanticTokensProvider(cronProvider); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java index c40e1a530..3189437c4 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/BootJavaLanguageServerComponents.java @@ -71,6 +71,7 @@ import org.springframework.ide.vscode.commons.languageserver.util.CodeLensHandle import org.springframework.ide.vscode.commons.languageserver.util.DocumentHighlightHandler; import org.springframework.ide.vscode.commons.languageserver.util.DocumentSymbolHandler; import org.springframework.ide.vscode.commons.languageserver.util.HoverHandler; +import org.springframework.ide.vscode.commons.languageserver.util.InlayHintHandler; import org.springframework.ide.vscode.commons.languageserver.util.ReferencesHandler; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; @@ -116,6 +117,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent private BootJavaCodeActionProvider codeActionProvider; private DocumentSymbolHandler docSymbolProvider; private JdtSemanticTokensHandler semanticTokensHandler; + private JdtInlayHintsHandler inlayHintsHandler; public BootJavaLanguageServerComponents(ApplicationContext appContext) { this.server = appContext.getBean(SimpleLanguageServer.class); @@ -176,7 +178,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent codeLensHandler = createCodeLensEngine(springSymbolIndex, projectFinder, server); - highlightsEngine = createDocumentHighlightEngine(springSymbolIndex); + highlightsEngine = createDocumentHighlightEngine(appContext); documents.onDocumentHighlight(highlightsEngine); Map jdtSemanticTokensProviders = appContext.getBeansOfType(JdtSemanticTokensProvider.class); @@ -184,6 +186,11 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent semanticTokensHandler = new JdtSemanticTokensHandler(cuCache, projectFinder, jdtSemanticTokensProviders.values()); } + Map jdtInlayHintsProviders = appContext.getBeansOfType(JdtInlayHintsProvider.class); + if (!jdtSemanticTokensProviders.isEmpty()) { + inlayHintsHandler = new JdtInlayHintsHandler(cuCache, projectFinder, jdtInlayHintsProviders.values()); + } + config.addListener(ignore -> { log.info("update live process tracker settings - start"); @@ -313,9 +320,14 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent return new BootJavaCodeLensEngine(this, codeLensProvider); } - protected BootJavaDocumentHighlightEngine createDocumentHighlightEngine(SpringSymbolIndex indexer) { + protected BootJavaDocumentHighlightEngine createDocumentHighlightEngine(ApplicationContext appContext) { Collection highlightProvider = new ArrayList<>(); - highlightProvider.add(new WebfluxRouteHighlightProdivder(indexer)); + highlightProvider.add(new WebfluxRouteHighlightProdivder(appContext.getBean(SpringSymbolIndex.class))); + + Map astHighlightProviders = appContext.getBeansOfType(JdtAstDocHighlightsProvider.class); + if (!astHighlightProviders.isEmpty()) { + highlightProvider.add(new JdtDocHighlightsProvider(projectFinder, cuCache, astHighlightProviders.values())); + } return new BootJavaDocumentHighlightEngine(this, highlightProvider); } @@ -359,5 +371,10 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent return Optional.ofNullable(semanticTokensHandler); } + @Override + public Optional getInlayHintHandler() { + return Optional.ofNullable(inlayHintsHandler); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/IJavaDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/IJavaDefinitionProvider.java index 372279690..98faa3f10 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/IJavaDefinitionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/IJavaDefinitionProvider.java @@ -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 @@ -15,11 +15,12 @@ import java.util.List; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.springframework.ide.vscode.commons.java.IJavaProject; public interface IJavaDefinitionProvider { - List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n); + List getDefinitions(CancelChecker cancelToken, IJavaProject project, TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JavaDefinitionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JavaDefinitionHandler.java index 81c0251dc..e1a3d96a8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JavaDefinitionHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JavaDefinitionHandler.java @@ -21,6 +21,8 @@ import org.eclipse.lsp4j.DefinitionParams; import org.eclipse.lsp4j.LocationLink; import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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; @@ -33,6 +35,8 @@ import com.google.common.collect.ImmutableList.Builder; public class JavaDefinitionHandler implements DefinitionHandler, LanguageSpecific { + private static final Logger log = LoggerFactory.getLogger(JavaDefinitionHandler.class); + private CompilationUnitCache cuCache; private JavaProjectFinder projectFinder; private Collection providers; @@ -64,7 +68,11 @@ public class JavaDefinitionHandler implements DefinitionHandler, LanguageSpecifi if (cancelToken.isCanceled()) { break; } - builder.addAll(provider.getDefinitions(cancelToken, project, cu, node)); + try { + builder.addAll(provider.getDefinitions(cancelToken, project, doc, cu, node, start)); + } catch (Exception e) { + log.error("", e); + } } } return builder.build(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtAstDocHighlightsProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtAstDocHighlightsProvider.java new file mode 100644 index 000000000..4c0fbfea1 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtAstDocHighlightsProvider.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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.List; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.lsp4j.DocumentHighlight; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public interface JdtAstDocHighlightsProvider { + + List getDocHighlights(IJavaProject project, TextDocument doc, CompilationUnit cu, ASTNode node, int offset); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtDocHighlightsProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtDocHighlightsProvider.java new file mode 100644 index 000000000..a0c3ed91a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtDocHighlightsProvider.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * 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 org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.NodeFinder; +import org.eclipse.lsp4j.DocumentHighlight; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.handlers.HighlightProvider; +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.util.text.TextDocument; + +public class JdtDocHighlightsProvider implements HighlightProvider { + + private static final Logger log = LoggerFactory.getLogger(JdtDocHighlightsProvider.class); + + private final JavaProjectFinder projectFinder; + private final CompilationUnitCache cuCache; + private final Collection astHighlightProviders; + + + public JdtDocHighlightsProvider(JavaProjectFinder projectFinder, CompilationUnitCache cuCache, Collection astHighlightProviders) { + this.projectFinder = projectFinder; + this.cuCache = cuCache; + this.astHighlightProviders = astHighlightProviders; + } + + @Override + public void provideHighlights(CancelChecker cancelToken, TextDocument doc, Position p, + List resultAccumulator) { + if (!astHighlightProviders.isEmpty()) { + IJavaProject project = projectFinder.find(doc.getId()).orElse(null); + if (project != null) { + URI docUri = URI.create(doc.getUri()); + cuCache.withCompilationUnit(project, docUri, cu -> { + if (cu != null) { + int start = cu.getPosition(p.getLine() + 1, p.getCharacter()); + ASTNode node = NodeFinder.perform(cu, start, 0); + for (JdtAstDocHighlightsProvider provider : astHighlightProviders) { + try { + resultAccumulator.addAll(provider.getDocHighlights(project, doc, cu, node, start)); + } catch (Throwable t) { + log.error("", t); + } + } + } + return null; + }); + } + } + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtInlayHintsHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtInlayHintsHandler.java new file mode 100644 index 000000000..1bd048b6c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtInlayHintsHandler.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * 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.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.lsp4j.InlayHint; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.boot.java.reconcilers.CompositeASTVisitor; +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.util.InlayHintHandler; +import org.springframework.ide.vscode.commons.util.Collector; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class JdtInlayHintsHandler implements InlayHintHandler { + + private final CompilationUnitCache cuCache; + private final JavaProjectFinder projectFinder; + private final Collection inlayHintsProviders; + + public JdtInlayHintsHandler(CompilationUnitCache cuCache, JavaProjectFinder projectFinder, Collection inlayHintsProviders) { + this.cuCache = cuCache; + this.projectFinder = projectFinder; + this.inlayHintsProviders = inlayHintsProviders; + } + + @Override + public List handle(TextDocument doc, Range range, CancelChecker cancelChecker) { + Optional optProject = projectFinder.find(doc.getId()); + if (optProject.isPresent()) { + IJavaProject jp = optProject.get(); + List applicableInlayHintsProviders = inlayHintsProviders.stream().filter(tp -> tp.isApplicable(jp)).collect(Collectors.toList()); + if (!applicableInlayHintsProviders.isEmpty()) { + return cuCache.withCompilationUnit(jp, URI.create(doc.getUri()), cu -> computeInlayHints(applicableInlayHintsProviders, jp, cu, range, doc)); + } + } + return Collections.emptyList(); + } + + private List computeInlayHints(List applicableInlayHintsProviders, IJavaProject jp, CompilationUnit cu, Range r, TextDocument doc) { + if (cu == null) { + return null; + } + Collector collector = new Collector<>(); + CompositeASTVisitor visitor = new CompositeASTVisitor(); + applicableInlayHintsProviders.forEach(p -> visitor.add(p.getInlayHintsComputer(jp, doc, 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); + return collector.get(); + } +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtInlayHintsProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtInlayHintsProvider.java new file mode 100644 index 000000000..7b65746e7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/JdtInlayHintsProvider.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * 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 org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.lsp4j.InlayHint; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.util.Collector; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public interface JdtInlayHintsProvider { + + boolean isApplicable(IJavaProject project); + ASTVisitor getInlayHintsComputer(IJavaProject project, TextDocument doc, CompilationUnit cu, Collector collector); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnDefinitionProvider.java index 7bf47e80c..26bc56fb9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnDefinitionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnDefinitionProvider.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.Annotations; @@ -41,7 +42,7 @@ public class DependsOnDefinitionProvider implements IJavaDefinitionProvider { } @Override - public List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n) { + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) { if (n instanceof StringLiteral) { StringLiteral valueNode = (StringLiteral) n; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierDefinitionProvider.java index cdf20c1c7..5b1feca35 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierDefinitionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierDefinitionProvider.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.Annotations; @@ -41,7 +42,7 @@ public class QualifierDefinitionProvider implements IJavaDefinitionProvider { } @Override - public List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n) { + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) { if (n instanceof StringLiteral) { StringLiteral valueNode = (StringLiteral) n; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceDefinitionProvider.java index 45af2eaf7..ebc9c815e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceDefinitionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/ResourceDefinitionProvider.java @@ -21,6 +21,7 @@ import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.IAnnotationBinding; import org.eclipse.jdt.core.dom.StringLiteral; import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; import org.springframework.ide.vscode.boot.java.Annotations; @@ -41,7 +42,7 @@ public class ResourceDefinitionProvider implements IJavaDefinitionProvider { } @Override - public List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n) { + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) { if (n instanceof StringLiteral) { StringLiteral valueNode = (StringLiteral) n; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java index 61fb0364a..951db75f5 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronReconciler.java @@ -14,11 +14,8 @@ import java.net.URI; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.NormalAnnotation; -import org.springframework.ide.vscode.boot.java.Annotations; -import org.springframework.ide.vscode.boot.java.data.jpa.queries.QueryJdtAstReconciler; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedExpression; import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; @@ -49,22 +46,9 @@ public class JdtCronReconciler implements JdtAstReconciler { return new ASTVisitor() { @Override public boolean visit(NormalAnnotation node) { - if (node.getTypeName() != null) { - String fqn = node.getTypeName().getFullyQualifiedName(); - if (JdtCronSemanticTokensProvider.SCHEDULED_SIMPLE_NAME.equals(fqn) || Annotations.SCHEDULED.equals(fqn)) { - ITypeBinding typeBinding = node.resolveTypeBinding(); - if (typeBinding != null && Annotations.SCHEDULED.equals(typeBinding.getQualifiedName())) { - for (Object value : node.values()) { - if (value instanceof MemberValuePair) { - MemberValuePair pair = (MemberValuePair) value; - String name = pair.getName().getFullyQualifiedName(); - if (name != null && "cron".equals(name) && JdtCronSemanticTokensProvider.isCronExpression(pair.getValue())) { - QueryJdtAstReconciler.reconcileExpression(cronReconciler, pair.getValue(), problemCollector); - } - } - } - } - } + EmbeddedExpression e = JdtCronVisitorUtils.extractCron(node); + if (e != null) { + cronReconciler.reconcile(e.text(), e.offset(), problemCollector); } return super.visit(node); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java index 040267c17..0921bd4a8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronSemanticTokensProvider.java @@ -14,15 +14,9 @@ import java.util.List; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; -import org.eclipse.jdt.core.dom.Expression; -import org.eclipse.jdt.core.dom.ITypeBinding; -import org.eclipse.jdt.core.dom.MemberValuePair; import org.eclipse.jdt.core.dom.NormalAnnotation; -import org.eclipse.jdt.core.dom.StringLiteral; -import org.eclipse.jdt.core.dom.TextBlock; -import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider; -import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtDataQuerySemanticTokensProvider; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedExpression; 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; @@ -62,22 +56,9 @@ public class JdtCronSemanticTokensProvider implements JdtSemanticTokensProvider @Override public boolean visit(NormalAnnotation node) { - if (node.getTypeName() != null) { - String fqn = node.getTypeName().getFullyQualifiedName(); - if (SCHEDULED_SIMPLE_NAME.equals(fqn) || Annotations.SCHEDULED.equals(fqn)) { - ITypeBinding typeBinding = node.resolveTypeBinding(); - if (typeBinding != null && Annotations.SCHEDULED.equals(typeBinding.getQualifiedName())) { - for (Object value : node.values()) { - if (value instanceof MemberValuePair) { - MemberValuePair pair = (MemberValuePair) value; - String name = pair.getName().getFullyQualifiedName(); - if (name != null && "cron".equals(name) && isCronExpression(pair.getValue())) { - JdtDataQuerySemanticTokensProvider.computeTokensForExpression(tokensProvider, pair.getValue()).forEach(collector::accept); - } - } - } - } - } + EmbeddedExpression e = JdtCronVisitorUtils.extractCron(node); + if (e != null) { + tokensProvider.computeTokens(e.text(), e.offset()).forEach(collector::accept); } return super.visit(node); } @@ -85,19 +66,4 @@ public class JdtCronSemanticTokensProvider implements JdtSemanticTokensProvider }; } - public static boolean isCronExpression(Expression e) { - String value = null; - if (e instanceof StringLiteral sl) { - value = sl.getLiteralValue(); - } else if (e instanceof TextBlock tb) { - value = tb.getLiteralValue(); - } - value = value.trim(); - if (value.startsWith("#{") || value.startsWith("${")) { - // Either SPEL or Property Holder - return false; - } - return value != null; - } - } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronVisitorUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronVisitorUtils.java new file mode 100644 index 000000000..2d11ca90f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/cron/JdtCronVisitorUtils.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * 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.cron; + +import static org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.extractEmbeddedExpression; + +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.TextBlock; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedExpression; + +public class JdtCronVisitorUtils { + + static final String SCHEDULED_SIMPLE_NAME = "Scheduled"; + + public static EmbeddedExpression extractCron(NormalAnnotation node) { + if (node.getTypeName() != null) { + String fqn = node.getTypeName().getFullyQualifiedName(); + if (SCHEDULED_SIMPLE_NAME.equals(fqn) || Annotations.SCHEDULED.equals(fqn)) { + ITypeBinding typeBinding = node.resolveTypeBinding(); + if (typeBinding != null && Annotations.SCHEDULED.equals(typeBinding.getQualifiedName())) { + for (Object value : node.values()) { + if (value instanceof MemberValuePair) { + MemberValuePair pair = (MemberValuePair) value; + String name = pair.getName().getFullyQualifiedName(); + if (name != null && "cron".equals(name) && isCronExpression(pair.getValue())) { + return extractEmbeddedExpression(pair.getValue()); + } + } + } + } + } + } + return null; + } + + private static boolean isCronExpression(Expression e) { + String value = null; + if (e instanceof StringLiteral sl) { + value = sl.getLiteralValue(); + } else if (e instanceof TextBlock tb) { + value = tb.getLiteralValue(); + } + value = value.trim(); + if (value.startsWith("#{") || value.startsWith("${")) { + // Either SPEL or Property Holder + return false; + } + return value != null; + } + + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProvider.java new file mode 100644 index 000000000..a14ccd19f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProvider.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * 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.Collections; +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.TextBlock; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.semantic.tokens.SemanticTokenData; +import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService; +import org.springframework.ide.vscode.commons.util.BadLocationException; +import org.springframework.ide.vscode.commons.util.Collector; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class DataQueryParameterDefinitionProvider implements IJavaDefinitionProvider { + + private static final Logger log = LoggerFactory.getLogger(DataQueryParameterDefinitionProvider.class); + + private final JdtDataQuerySemanticTokensProvider semanticTokensProvider; + private final SimpleTextDocumentService documents; + + public DataQueryParameterDefinitionProvider(SimpleTextDocumentService documents, JdtDataQuerySemanticTokensProvider semanticTokensProvider) { + this.documents = documents; + this.semanticTokensProvider = semanticTokensProvider; + } + + @Override + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, + TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) { + if (n instanceof StringLiteral || n instanceof TextBlock) { + + ASTNode a = JdtQueryDocHighlightsProvider.findQueryAnnotation(n); + + TextDocument doc = documents.getLatestSnapshot(docId.getUri()); + + if (a.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) { + Collector collector = new Collector<>(); + a.accept(semanticTokensProvider.getTokensComputer(project, doc, cu, collector)); + for (SemanticTokenData t : collector.get()) { + if ("parameter".equals(t.type()) && t.start() <= offset && offset <= t.end()) { + try { + String parameterDescriptor = doc.get(t.start(), t.end() - t.start()); + SimpleName paramName = JdtQueryDocHighlightsProvider.findParameter(m, parameterDescriptor); + if (paramName != null) { + LocationLink link = new LocationLink(); + link.setTargetUri(docId.getUri()); + link.setOriginSelectionRange(doc.toRange(t.start(), t.end() - t.start())); + link.setTargetSelectionRange(doc.toRange(paramName.getStartPosition(), paramName.getLength())); + link.setTargetRange(link.getTargetSelectionRange()); + return List.of(link); + } + } catch (BadLocationException e) { + log.error("", e); + } + } + } + } + + } + return Collections.emptyList(); + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlSemanticTokens.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlSemanticTokens.java index 7c3d32c87..4a2a06cfb 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlSemanticTokens.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/HqlSemanticTokens.java @@ -146,6 +146,9 @@ public class HqlSemanticTokens implements SemanticTokensDataProvider { if (ctx.identifier() != null && ctx.identifier().getStart() != null) { semantics.put(ctx.identifier().getStart(), "parameter"); } + if (ctx.INTEGER_LITERAL() != null) { + semantics.put(ctx.INTEGER_LITERAL().getSymbol(), "parameter"); + } } }); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQueriesInlayHintsProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQueriesInlayHintsProvider.java new file mode 100644 index 000000000..49e90fe7c --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtDataQueriesInlayHintsProvider.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * 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.List; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.lsp4j.InlayHint; +import org.eclipse.lsp4j.InlayHintKind; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.springframework.ide.vscode.boot.java.JdtInlayHintsProvider; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression; +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.BadLocationException; +import org.springframework.ide.vscode.commons.util.Collector; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class JdtDataQueriesInlayHintsProvider implements JdtInlayHintsProvider { + + private final JdtDataQuerySemanticTokensProvider semanticTokensProvider; + + + public JdtDataQueriesInlayHintsProvider(JdtDataQuerySemanticTokensProvider semanticTokensProvider) { + this.semanticTokensProvider = semanticTokensProvider; + } + + @Override + public boolean isApplicable(IJavaProject project) { + return semanticTokensProvider.isApplicable(project); + } + + @Override + public ASTVisitor getInlayHintsComputer(IJavaProject project, TextDocument doc, CompilationUnit cu, + Collector collector) { + return new ASTVisitor() { + + @Override + public boolean visit(NormalAnnotation node) { + if (node.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) { + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + if (q != null) { + processQuery(project, doc, collector, m, q); + } + } + return super.visit(node); + } + + @Override + public boolean visit(SingleMemberAnnotation node) { + if (node.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) { + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + if (q != null) { + processQuery(project, doc, collector, m, q); + } + } + return super.visit(node); + } + + }; + } + + private void processQuery(IJavaProject project, TextDocument doc, Collector collector, MethodDeclaration m, EmbeddedQueryExpression q) { + List semanticTokens = semanticTokensProvider.computeSemanticTokens(project, q.query().text(), q.query().offset(), q.isNative()); + for (SemanticTokenData t : semanticTokens) { + if ("parameter".equals(t.type())) { + try { + int number = Integer.parseInt(doc.get(t.start(), t.end() - t.start())); + if (number > 0 && number <= m.parameters().size()) { + Object param = m.parameters().get(number - 1); + if (param instanceof SingleVariableDeclaration svd) { + String paramName = svd.getName().getIdentifier(); + InlayHint hint = new InlayHint(); + hint.setKind(InlayHintKind.Parameter); + hint.setLabel(Either.forLeft(paramName)); + hint.setPaddingLeft(true); + hint.setPosition(doc.toPosition(t.end())); + + collector.accept(hint); + } + } + } catch (NumberFormatException e) { + // ignore + } catch (BadLocationException e) { + // ignore + } + } + } + } + +} 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 d1702a41c..bac9c8d76 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 @@ -19,20 +19,12 @@ import java.util.stream.Collectors; import java.util.stream.Stream; 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.IMethodBinding; -import org.eclipse.jdt.core.dom.ITypeBinding; -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.eclipse.jdt.core.dom.TextBlock; -import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.JdtSemanticTokensProvider; -import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression; import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.java.SpringProjectUtil; @@ -43,9 +35,6 @@ import org.springframework.ide.vscode.commons.util.text.TextDocument; public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProvider { - private static final String QUERY = "Query"; - private static final String NAMED_QUERY = "NamedQuery"; - private final JpqlSemanticTokens jpqlProvider; private final HqlSemanticTokens hqlProvider; private final JpqlSupportState supportState; @@ -74,126 +63,43 @@ public class JdtDataQuerySemanticTokensProvider implements JdtSemanticTokensProv @Override public ASTVisitor getTokensComputer(IJavaProject jp, TextDocument doc, CompilationUnit cu, Collector tokensData) { - SemanticTokensDataProvider provider = SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? hqlProvider : jpqlProvider; return new ASTVisitor() { @Override public boolean visit(NormalAnnotation a) { - Expression queryExpression = null; - boolean isNative = false; - if (isQueryAnnotation(a)) { - for (Object value : a.values()) { - if (value instanceof MemberValuePair) { - MemberValuePair pair = (MemberValuePair) value; - String name = pair.getName().getFullyQualifiedName(); - if (name != null) { - switch (name) { - case "value": - queryExpression = pair.getValue(); - break; - case "nativeQuery": - Expression expression = pair.getValue(); - if (expression != null) { - Object o = expression.resolveConstantExpressionValue(); - if (o instanceof Boolean b) { - isNative = b.booleanValue(); - } - } - break; - } - } - } - } - } else if (isNamedQueryAnnotation(a)) { - for (Object value : a.values()) { - if (value instanceof MemberValuePair) { - MemberValuePair pair = (MemberValuePair) value; - String name = pair.getName().getFullyQualifiedName(); - if (name != null) { - switch (name) { - case "query": - queryExpression = pair.getValue(); - break; - } - } - } - } + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(a); + if (q != null) { + computeSemanticTokens(jp, q.query().text(), q.query().offset(), q.isNative()).forEach(tokensData::accept); } - - if (queryExpression != null) { - if (isNative) { - computeTokensForExpression(getSqlSemanticTokensProvider(jp), queryExpression).forEach(tokensData::accept); - } else { - computeTokensForExpression(provider, queryExpression).forEach(tokensData::accept); - } - } - - return false; + return super.visit(a); } @Override public boolean visit(SingleMemberAnnotation a) { - if (isQueryAnnotation(a)) { - computeTokensForExpression(provider, a.getValue()).forEach(tokensData::accept); + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(a); + if (q != null) { + computeSemanticTokens(jp, q.query().text(), q.query().offset(), q.isNative()).forEach(tokensData::accept); } - return false; + return super.visit(a); } @Override public boolean visit(MethodInvocation node) { - if ("createQuery".equals(node.getName().getIdentifier()) && node.arguments().size() <= 2 && node.arguments().get(0) instanceof Expression 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())) { - computeTokensForExpression(provider, queryExpr).forEach(tokensData::accept); - } - } + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + if (q != null) { + computeSemanticTokens(jp, q.query().text(), q.query().offset(), q.isNative()).forEach(tokensData::accept); } return super.visit(node); } }; } - public static List computeTokensForExpression(SemanticTokensDataProvider provider, Expression valueExp) { - String query = null; - int offset = 0; - if (valueExp instanceof StringLiteral sl) { - query = sl.getEscapedValue(); - query = query.substring(1, query.length() - 1); - offset = sl.getStartPosition() + 1; // +1 to skip over opening " - } else if (valueExp instanceof TextBlock tb) { - query = tb.getEscapedValue(); - query = query.substring(3, query.length() - 3); - offset = tb.getStartPosition() + 3; // +3 to skip over opening """ - } - - if (query != null) { + public List computeSemanticTokens(IJavaProject jp, String query, int offset, boolean isNative) { + SemanticTokensDataProvider provider = isNative ? getSqlSemanticTokensProvider(jp) : (SpringProjectUtil.hasDependencyStartingWith(jp, "hibernate-core", null) ? hqlProvider : jpqlProvider); + if (provider != null) { return provider.computeTokens(query, offset); } return Collections.emptyList(); } - - - static boolean isQueryAnnotation(Annotation a) { - if (Annotations.DATA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) { - ITypeBinding type = a.resolveTypeBinding(); - if (type != null) { - return AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.DATA_QUERY); - } - } - return false; - } - - static boolean isNamedQueryAnnotation(Annotation a) { - if (NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || Annotations.JPA_JAKARTA_NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName()) - || Annotations.JPA_JAVAX_NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName())) { - ITypeBinding type = a.resolveTypeBinding(); - if (type != null) { - return AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.JPA_JAKARTA_NAMED_QUERY) - || AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.JPA_JAVAX_NAMED_QUERY); - } - } - return false; - } @Override public boolean isApplicable(IJavaProject project) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProvider.java new file mode 100644 index 000000000..68b6e2e32 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProvider.java @@ -0,0 +1,111 @@ +/******************************************************************************* + * 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.Collections; +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.MemberValuePair; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.NormalAnnotation; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.TextBlock; +import org.eclipse.jdt.core.dom.VariableDeclaration; +import org.eclipse.lsp4j.DocumentHighlight; +import org.eclipse.lsp4j.DocumentHighlightKind; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.JdtAstDocHighlightsProvider; +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.BadLocationException; +import org.springframework.ide.vscode.commons.util.Collector; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class JdtQueryDocHighlightsProvider implements JdtAstDocHighlightsProvider { + + private static final Logger log = LoggerFactory.getLogger(JdtQueryDocHighlightsProvider.class); + + private JdtDataQuerySemanticTokensProvider semanticTokensProvider; + + public JdtQueryDocHighlightsProvider(JdtDataQuerySemanticTokensProvider semanticTokensProvider) { + this.semanticTokensProvider = semanticTokensProvider; + } + + @Override + public List getDocHighlights(IJavaProject project, TextDocument doc, CompilationUnit cu, + ASTNode node, int offset) { + if (node instanceof StringLiteral || node instanceof TextBlock) { + Annotation a = findQueryAnnotation(node); + if (a != null && a.getParent() instanceof MethodDeclaration m && !m.parameters().isEmpty()) { + Collector collector = new Collector<>(); + a.accept(semanticTokensProvider.getTokensComputer(project, doc, cu, collector)); + for (SemanticTokenData t : collector.get()) { + if ("parameter".equals(t.type()) && t.start() <= offset && offset <= t.end()) { + try { + String parameterDescriptor = doc.get(t.start(), t.end() - t.start()); + SimpleName paramName = findParameter(m, parameterDescriptor); + if (paramName != null) { + DocumentHighlight highlight = new DocumentHighlight(); + highlight.setKind(DocumentHighlightKind.Write); + highlight.setRange(doc.toRange(paramName.getStartPosition(), paramName.getLength())); + return List.of(highlight); + } + } catch (BadLocationException e) { + log.error("", e); + } + } + } + } + } + return Collections.emptyList(); + } + + static Annotation findQueryAnnotation(ASTNode node) { + if (node.getParent() instanceof MemberValuePair pair + && node.getParent().getParent() instanceof NormalAnnotation na && "value".equals(pair.getName().getIdentifier()) + && JdtQueryVisitorUtils.isQueryAnnotation(na)) { + return na; + } else if (node.getParent() instanceof SingleMemberAnnotation sm + && JdtQueryVisitorUtils.isQueryAnnotation(sm)) { + return sm; + } + return null; + } + + static SimpleName findParameter(MethodDeclaration m, String p) { + try { + int paramNumber = Integer.parseInt(p); + if (paramNumber > 0 && paramNumber <= m.parameters().size()) { + Object o = m.parameters().get(paramNumber - 1); + if (o instanceof VariableDeclaration vd) { + return vd.getName(); + } + } + } catch (NumberFormatException e) { + for (Object o : m.parameters()) { + if (o instanceof VariableDeclaration vd) { + SimpleName simpleName = vd.getName(); + if (simpleName != null && p.equals(simpleName.getIdentifier())) { + return simpleName; + } + } + } + } + return null; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java new file mode 100644 index 000000000..ed93a516f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryVisitorUtils.java @@ -0,0 +1,145 @@ +/******************************************************************************* + * 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.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; +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.eclipse.jdt.core.dom.TextBlock; +import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies; + +public class JdtQueryVisitorUtils { + + private static final String QUERY = "Query"; + private static final String NAMED_QUERY = "NamedQuery"; + + + public record EmbeddedExpression(Expression expression, String text, int offset) {}; + + public record EmbeddedQueryExpression(EmbeddedExpression query, boolean isNative) {}; + + public static EmbeddedQueryExpression extractQueryExpression(SingleMemberAnnotation a) { + if (isQueryAnnotation(a)) { + EmbeddedExpression expression = extractEmbeddedExpression(a.getValue()); + return expression == null ? null : new EmbeddedQueryExpression(expression, false); + } + return null; + } + + public static EmbeddedQueryExpression extractQueryExpression(NormalAnnotation a) { + Expression queryExpression = null; + boolean isNative = false; + if (isQueryAnnotation(a)) { + for (Object value : a.values()) { + if (value instanceof MemberValuePair) { + MemberValuePair pair = (MemberValuePair) value; + String name = pair.getName().getFullyQualifiedName(); + if (name != null) { + switch (name) { + case "value": + queryExpression = pair.getValue(); + break; + case "nativeQuery": + Expression expression = pair.getValue(); + if (expression != null) { + Object o = expression.resolveConstantExpressionValue(); + if (o instanceof Boolean b) { + isNative = b.booleanValue(); + } + } + break; + } + } + } + } + } else if (isNamedQueryAnnotation(a)) { + for (Object value : a.values()) { + if (value instanceof MemberValuePair) { + MemberValuePair pair = (MemberValuePair) value; + String name = pair.getName().getFullyQualifiedName(); + if (name != null) { + switch (name) { + case "query": + queryExpression = pair.getValue(); + break; + } + } + } + } + } + if (queryExpression != null) { + EmbeddedExpression e = extractEmbeddedExpression(queryExpression); + if (e != null) { + return new EmbeddedQueryExpression(e, isNative); + } + } + return null; + } + + public static EmbeddedQueryExpression extractQueryExpression(MethodInvocation m) { + if ("createQuery".equals(m.getName().getIdentifier()) && m.arguments().size() <= 2 && m.arguments().get(0) instanceof Expression queryExpr) { + IMethodBinding methodBinding = m.resolveMethodBinding(); + if ("jakarta.persistence.EntityManager".equals(methodBinding.getDeclaringClass().getQualifiedName())) { + if (methodBinding.getParameterTypes().length <= 2 && "java.lang.String".equals(methodBinding.getParameterTypes()[0].getQualifiedName())) { + EmbeddedExpression expression = extractEmbeddedExpression(queryExpr); + return expression == null ? null : new EmbeddedQueryExpression(expression, false); + } + } + } + return null; + } + + public static EmbeddedExpression extractEmbeddedExpression(Expression valueExp) { + String text = null; + int offset = 0; + if (valueExp instanceof StringLiteral sl) { + text = sl.getEscapedValue(); + text = text.substring(1, text.length() - 1); + offset = sl.getStartPosition() + 1; // +1 to skip over opening " + } else if (valueExp instanceof TextBlock tb) { + text = tb.getEscapedValue(); + text = text.substring(3, text.length() - 3); + offset = tb.getStartPosition() + 3; // +3 to skip over opening """ + } + return text == null ? null : new EmbeddedExpression(valueExp, text, offset); + } + + + static boolean isQueryAnnotation(Annotation a) { + if (Annotations.DATA_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || QUERY.equals(a.getTypeName().getFullyQualifiedName())) { + ITypeBinding type = a.resolveTypeBinding(); + if (type != null) { + return AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.DATA_QUERY); + } + } + return false; + } + + static boolean isNamedQueryAnnotation(Annotation a) { + if (NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName()) || Annotations.JPA_JAKARTA_NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName()) + || Annotations.JPA_JAVAX_NAMED_QUERY.equals(a.getTypeName().getFullyQualifiedName())) { + ITypeBinding type = a.resolveTypeBinding(); + if (type != null) { + return AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.JPA_JAKARTA_NAMED_QUERY) + || AnnotationHierarchies.hasTransitiveSuperAnnotationType(type, Annotations.JPA_JAVAX_NAMED_QUERY); + } + } + return false; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSemanticTokens.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSemanticTokens.java index fff3fc553..99a1d7197 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSemanticTokens.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JpqlSemanticTokens.java @@ -157,6 +157,9 @@ public class JpqlSemanticTokens implements SemanticTokensDataProvider { if (ctx.identification_variable() != null && ctx.identification_variable().getStart() != null) { semantics.put(ctx.identification_variable().getStart(), "parameter"); } + if (ctx.INTLITERAL() != null) { + semantics.put(ctx.INTLITERAL().getSymbol(), "parameter"); + } } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java index b592bad74..bea37b4e8 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/QueryJdtAstReconciler.java @@ -18,13 +18,12 @@ import java.util.Optional; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.Expression; -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.eclipse.jdt.core.dom.TextBlock; +import org.springframework.ide.vscode.boot.java.data.jpa.queries.JdtQueryVisitorUtils.EmbeddedQueryExpression; import org.springframework.ide.vscode.boot.java.handlers.Reconciler; import org.springframework.ide.vscode.boot.java.reconcilers.JdtAstReconciler; import org.springframework.ide.vscode.boot.java.reconcilers.RequiredCompleteAstException; @@ -61,77 +60,28 @@ public class QueryJdtAstReconciler implements JdtAstReconciler { @Override public boolean visit(NormalAnnotation node) { - - Expression queryExpression = null; - boolean isNative = false; - if (JdtDataQuerySemanticTokensProvider.isQueryAnnotation(node)) { - for (Object value : node.values()) { - if (value instanceof MemberValuePair) { - MemberValuePair pair = (MemberValuePair) value; - String name = pair.getName().getFullyQualifiedName(); - if (name != null) { - switch (name) { - case "value": - queryExpression = pair.getValue(); - break; - case "nativeQuery": - Expression expression = pair.getValue(); - if (expression != null) { - Object o = expression.resolveConstantExpressionValue(); - if (o instanceof Boolean b) { - isNative = b.booleanValue(); - } - } - break; - } - } - } - } - } else if (JdtDataQuerySemanticTokensProvider.isNamedQueryAnnotation(node)) { - for (Object value : node.values()) { - if (value instanceof MemberValuePair) { - MemberValuePair pair = (MemberValuePair) value; - String name = pair.getName().getFullyQualifiedName(); - if (name != null) { - switch (name) { - case "query": - queryExpression = pair.getValue(); - break; - } - } - } - } + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + if (q != null) { + Optional reconcilerOpt = q.isNative() ? getSqlReconciler(project) : Optional.of(getQueryReconciler(project)); + reconcilerOpt.ifPresent(r -> r.reconcile(q.query().text(), q.query().offset(), problemCollector)); } - - if (queryExpression != null) { - if (isNative) { - final Expression expr = queryExpression; - getSqlReconciler(project).ifPresent(r -> reconcileExpression(r, expr, problemCollector)); - } else { - reconcileExpression(getQueryReconciler(project), queryExpression, problemCollector); - } - } - - return false; + return super.visit(node); } @Override public boolean visit(SingleMemberAnnotation node) { - if (JdtDataQuerySemanticTokensProvider.isQueryAnnotation(node)) { - reconcileExpression(getQueryReconciler(project), node.getValue(), problemCollector); + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + if (q != null) { + getQueryReconciler(project).reconcile(q.query().text(), q.query().offset(), problemCollector); } - return false; + return super.visit(node); } @Override public boolean visit(MethodInvocation node) { - if ("createQuery".equals(node.getName().getIdentifier()) && node.arguments().size() <= 2 && node.arguments().get(0) instanceof Expression 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())) { - reconcileExpression(getQueryReconciler(project), queryExpr, problemCollector); - } - } + EmbeddedQueryExpression q = JdtQueryVisitorUtils.extractQueryExpression(node); + if (q != null) { + getQueryReconciler(project).reconcile(q.query().text(), q.query().offset(), problemCollector); } return super.visit(node); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueDefinitionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueDefinitionProvider.java index 780f1540b..c90cc7bc7 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueDefinitionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/value/ValueDefinitionProvider.java @@ -10,10 +10,8 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.value; -import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -32,6 +30,7 @@ import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; import org.eclipse.lsp4j.Position; import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,7 +79,8 @@ public class ValueDefinitionProvider implements IJavaDefinitionProvider { ); @Override - public List getDefinitions(CancelChecker cancelToken, IJavaProject project, CompilationUnit cu, ASTNode n) { + public List getDefinitions(CancelChecker cancelToken, IJavaProject project, + TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) { if (n instanceof StringLiteral) { StringLiteral valueNode = (StringLiteral) n; @@ -283,6 +283,4 @@ public class ValueDefinitionProvider implements IJavaDefinitionProvider { return resources; } - - } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandler.java index d699e3180..6e1bb0b7a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandler.java @@ -28,8 +28,8 @@ import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintKind; import org.eclipse.lsp4j.InlayHintLabelPart; -import org.eclipse.lsp4j.InlayHintParams; import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,12 +55,10 @@ public class PomInlayHintHandler implements InlayHintHandler { private static final Logger log = LoggerFactory.getLogger(PomInlayHintHandler.class); - final private SimpleLanguageServer server; final private JavaProjectFinder projectFinder; final private SpringProjectsProvider generationsProvider; public PomInlayHintHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder, ProjectObserver projectObserver, SpringProjectsProvider generationsProvider) { - this.server = server; this.projectFinder = projectFinder; this.generationsProvider = generationsProvider; @@ -99,13 +97,13 @@ public class PomInlayHintHandler implements InlayHintHandler { } @Override - public List handle(CancelChecker token, InlayHintParams params) { - URI uri = URI.create(params.getTextDocument().getUri()); + public List handle(TextDocument doc, Range range, CancelChecker cancelChecker) { + URI uri = URI.create(doc.getUri()); if ("file".equals(uri.getScheme()) && POM_XML.equals(Paths.get(uri).getFileName().toString())) { List inlayHintProviders = new ArrayList<>(); - Optional projectOpt = projectFinder.find(params.getTextDocument()); + Optional projectOpt = projectFinder.find(doc.getId()); if (projectOpt.isPresent()) { IJavaProject jp = projectOpt.get(); @@ -194,20 +192,15 @@ public class PomInlayHintHandler implements InlayHintHandler { } if (!inlayHintProviders.isEmpty()) { - TextDocument doc = server.getTextDocumentService().getLatestSnapshot(params.getTextDocument().getUri()); - - if (doc != null) { - String content = doc.get(); - if (!content.isEmpty()) { - // if doc is not empty, dive into the details and provide more sophisticated content assist proposals - DOMParser parser = DOMParser.getInstance(); - DOMDocument dom = parser.parse(content, "", null); + String content = doc.get(); + if (!content.isEmpty()) { + DOMParser parser = DOMParser.getInstance(); + DOMDocument dom = parser.parse(content, "", null); - DOMElement project = dom.getDocumentElement(); + DOMElement project = dom.getDocumentElement(); - if (project != null && "project".equals(project.getTagName())) { - return inlayHintProviders.stream().flatMap(provider -> provider.computeInlayHints(doc, project).stream()).collect(Collectors.toList()); - } + if (project != null && "project".equals(project.getTagName())) { + return inlayHintProviders.stream().flatMap(provider -> provider.computeInlayHints(doc, project).stream()).collect(Collectors.toList()); } } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProviderTest.java new file mode 100644 index 000000000..70a7cfa0f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/DataQueryParameterDefinitionProviderTest.java @@ -0,0 +1,90 @@ +/******************************************************************************* + * 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.nio.file.Paths; + +import org.eclipse.lsp4j.Range; +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.commons.maven.java.MavenJavaProject; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +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 DataQueryParameterDefinitionProviderTest { + + @Autowired BootLanguageServerHarness harness; + + private ProjectsHarness projects = ProjectsHarness.INSTANCE; + + private MavenJavaProject jp; + + @BeforeEach + public void setup() throws Exception { + jp = projects.mavenProject("boot-mysql"); + harness.useProject(jp); + } + + @Test + void parameterNameDefinition() throws Exception { + String source = """ + package my.package + + import org.springframework.data.jpa.repository.Query; + + public interface OwnerRepository { + + @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName%") + Object findByLastName(@Param("lastName") String lastName); + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, source, Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString()); + + Range expectedRange = editor.rangeOf("String lastName", "lastName"); + Range highlightRange = editor.rangeOf(":lastName%", "lastName"); + editor.assertGotoDefinition(highlightRange.getStart(), expectedRange, highlightRange); + + } + + @Test + void parameterOrdinalDefinition() throws Exception { + String source = """ + package my.package + + import org.springframework.data.jpa.repository.Query; + + public interface OwnerRepository { + + @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :1%") + Object findByLastName(@Param("lastName") String lastName); + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, source, Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString()); + + Range expectedRange = editor.rangeOf("String lastName", "lastName"); + Range highlightRange = editor.rangeOf(":1%", "1"); + editor.assertGotoDefinition(highlightRange.getStart(), expectedRange, highlightRange); + + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProviderTest.java new file mode 100644 index 000000000..144017969 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/jpa/queries/JdtQueryDocHighlightsProviderTest.java @@ -0,0 +1,117 @@ +package org.springframework.ide.vscode.boot.java.data.jpa.queries; + +import java.nio.file.Paths; + +import org.eclipse.lsp4j.DocumentHighlight; +import org.eclipse.lsp4j.DocumentHighlightKind; +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.commons.maven.java.MavenJavaProject; +import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.Editor; +import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness; +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 JdtQueryDocHighlightsProviderTest { + + @Autowired BootLanguageServerHarness harness; + + private ProjectsHarness projects = ProjectsHarness.INSTANCE; + + private MavenJavaProject jp; + + @BeforeEach + public void setup() throws Exception { + jp = projects.mavenProject("boot-mysql"); + harness.useProject(jp); + } + + @Test + void parameterName() throws Exception { + String source = """ + package my.package + + import org.springframework.data.jpa.repository.Query; + + public interface OwnerRepository { + + @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName%") + Object findByLastName(@Param("lastName") String lastName); + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, source, Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString()); + + editor.assertDocumentHighlights(":lastName", new DocumentHighlight(editor.rangeOf("String lastName", "lastName"), DocumentHighlightKind.Write)); + + } + + @Test + void parameterOrdinal() throws Exception { + String source = """ + package my.package + + import org.springframework.data.jpa.repository.Query; + + public interface OwnerRepository { + + @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :1%") + Object findByLastName(@Param("lastName") String lastName); + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, source, Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString()); + + editor.assertDocumentHighlights(":1", new DocumentHighlight(editor.rangeOf("String lastName", "lastName"), DocumentHighlightKind.Write)); + + } + + @Test + void noOrdinalMatch() throws Exception { + String source = """ + package my.package + + import org.springframework.data.jpa.repository.Query; + + public interface OwnerRepository { + + @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :2%") + Object findByLastName(@Param("lastName") String lastName); + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, source, Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString()); + + editor.assertDocumentHighlights(":1"); + + } + + @Test + void noParameterNameMatch() throws Exception { + String source = """ + package my.package + + import org.springframework.data.jpa.repository.Query; + + public interface OwnerRepository { + + @Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :name%") + Object findByLastName(@Param("lastName") String lastName); + } + """; + + Editor editor = harness.newEditor(LanguageId.JAVA, source, Paths.get(jp.getLocationUri()).resolve("src/main/resource/my/package/OwnerRepository.java").toUri().toASCIIString()); + + editor.assertDocumentHighlights(":name"); + + } +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandlerTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandlerTest.java index bdaf67139..62f065292 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandlerTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/maven/PomInlayHintHandlerTest.java @@ -28,9 +28,7 @@ import java.util.Optional; import org.eclipse.lsp4j.Command; import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintLabelPart; -import org.eclipse.lsp4j.InlayHintParams; import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.jsonrpc.CancelChecker; import org.junit.jupiter.api.Test; import org.springframework.ide.vscode.boot.validation.generations.SpringProjectsProvider; @@ -79,7 +77,7 @@ public class PomInlayHintHandlerTest { PomInlayHintHandler inlayHanlder = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); - List hints = inlayHanlder.handle(mock(CancelChecker.class), new InlayHintParams(new TextDocumentIdentifier(doc.getUri()), doc.toRange(0, doc.getLength()))); + List hints = inlayHanlder.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(1, hints.size()); @@ -131,7 +129,7 @@ public class PomInlayHintHandlerTest { PomInlayHintHandler inlayHanlder = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); - List hints = inlayHanlder.handle(mock(CancelChecker.class), new InlayHintParams(new TextDocumentIdentifier(doc.getUri()), doc.toRange(0, doc.getLength()))); + List hints = inlayHanlder.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); @@ -169,7 +167,7 @@ public class PomInlayHintHandlerTest { PomInlayHintHandler inlayHanlder = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); - List hints = inlayHanlder.handle(mock(CancelChecker.class), new InlayHintParams(new TextDocumentIdentifier(doc.getUri()), doc.toRange(0, doc.getLength()))); + List hints = inlayHanlder.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(1, hints.size()); @@ -225,7 +223,7 @@ public class PomInlayHintHandlerTest { PomInlayHintHandler inlayHanlder = new PomInlayHintHandler(server, projectFinder, ProjectObserver.NULL, projectProvider); - List hints = inlayHanlder.handle(mock(CancelChecker.class), new InlayHintParams(new TextDocumentIdentifier(doc.getUri()), doc.toRange(0, doc.getLength()))); + List hints = inlayHanlder.handle(doc, doc.toRange(0, doc.getLength()), mock(CancelChecker.class)); assertEquals(0, hints.size()); }