diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/BasicCollector.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/BasicCollector.java new file mode 100644 index 000000000..3243a6941 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/BasicCollector.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 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.commons.languageserver.reconcile; + +import java.util.Collection; + +public class BasicCollector implements ICollector { + + private final Collection collection; + + public BasicCollector(Collection collection) { + this.collection = collection; + } + + @Override + public void beginCollecting() { + } + + @Override + public void endCollecting() { + } + + @Override + public void accept(T t) { + collection.add(t); + } + +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/ICollector.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/ICollector.java new file mode 100644 index 000000000..ab272c780 --- /dev/null +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/ICollector.java @@ -0,0 +1,35 @@ +/******************************************************************************* + * Copyright (c) 2025 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.commons.languageserver.reconcile; + +public interface ICollector { + + void beginCollecting(); + void endCollecting(); + void accept(T t); + + /** + * Optional for both implementors and callers. + *

+ * This method optionally allows callers to do partial collection between the + * start and end collecting, and can be called numerous times. The caller is + * responsible to decide when and how often these checkpoints are invoked during + * a collecting session. + *

+ * For implementors, this optional support handles cases where problems need to be processed in + * intermediate phases between the start and end collecting stages, and if + * implemented, should support multiple checkpoint invocations. + */ + default void checkPointCollecting() { + + } + +} diff --git a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/IProblemCollector.java b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/IProblemCollector.java index 0b4264c99..a409eb557 100644 --- a/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/IProblemCollector.java +++ b/headless-services/commons/commons-language-server/src/main/java/org/springframework/ide/vscode/commons/languageserver/reconcile/IProblemCollector.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2014-2017 Pivotal, Inc. + * Copyright (c) 2014, 2025 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,27 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.commons.languageserver.reconcile; -public interface IProblemCollector { - - void beginCollecting(); - void endCollecting(); - void accept(ReconcileProblem problem); - - /** - * Optional for both implementors and callers. - *

- * This method optionally allows callers to do partial collection between the - * start and end collecting, and can be called numerous times. The caller is - * responsible to decide when and how often these checkpoints are invoked during - * a collecting session. - *

- * For implementors, this optional support handles cases where problems need to be processed in - * intermediate phases between the start and end collecting stages, and if - * implemented, should support multiple checkpoint invocations. - */ - default void checkPointCollecting() { - - } +public interface IProblemCollector extends ICollector { /** * Problem collector that simply ignores/discards anything passed to it. diff --git a/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/AddAnnotationOverMethod.java b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/AddAnnotationOverMethod.java new file mode 100644 index 000000000..1ec9b7807 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/main/java/org/springframework/ide/vscode/commons/rewrite/java/AddAnnotationOverMethod.java @@ -0,0 +1,113 @@ +/******************************************************************************* + * Copyright (c) 2025 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.commons.rewrite.java; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.openrewrite.ExecutionContext; +import org.openrewrite.NlsRewrite.Description; +import org.openrewrite.NlsRewrite.DisplayName; +import org.openrewrite.Option; +import org.openrewrite.Preconditions; +import org.openrewrite.Recipe; +import org.openrewrite.Tree; +import org.openrewrite.TreeVisitor; +import org.openrewrite.java.AddOrUpdateAnnotationAttribute; +import org.openrewrite.java.JavaIsoVisitor; +import org.openrewrite.java.MethodMatcher; +import org.openrewrite.java.search.DeclaresMethod; +import org.openrewrite.java.tree.J; +import org.openrewrite.java.tree.J.Annotation; +import org.openrewrite.java.tree.J.MethodDeclaration; +import org.openrewrite.java.tree.JavaType; +import org.openrewrite.java.tree.Space; +import org.openrewrite.java.tree.TypeUtils; +import org.openrewrite.marker.Markers; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AddAnnotationOverMethod extends Recipe { + + public record Attribute(String name, String value) {} + + @Override + public @DisplayName String getDisplayName() { + return "Add annotation over method"; + } + + @Override + public @Description String getDescription() { + return "Add annotation over method."; + } + + @Option(description = "Method pattern", example = "com.example.Person setAge(int)") + private String method; + + @Option(description = "Annotation type") + private String annotationType; + + @Nullable + @Option(description = "Annotation attributes", required = false) + private List attributes; + + @JsonCreator + public AddAnnotationOverMethod( + @JsonProperty("method") String method, + @JsonProperty("annotationType") String annotationType, + @JsonProperty("attributes") @Nullable List attributes) { + this.method = method; + this.annotationType = annotationType; + this.attributes = attributes; + } + + @Override + public TreeVisitor getVisitor() { + final MethodMatcher matcher = new MethodMatcher(method); + return Preconditions.check(new DeclaresMethod<>(matcher), new JavaIsoVisitor<>() { + @Override + public MethodDeclaration visitMethodDeclaration(MethodDeclaration method, ExecutionContext ctx) { + MethodDeclaration m = super.visitMethodDeclaration(method, ctx); + if (matcher.matches(m.getMethodType())) { + Optional optAnnotation = m.getLeadingAnnotations().stream().filter(a -> TypeUtils.isOfClassType(a.getType(), annotationType)).findFirst(); + if (optAnnotation.isEmpty()) { + List annotations = new ArrayList<>(m.getLeadingAnnotations()); + JavaType.ShallowClass at = JavaType.ShallowClass.build(annotationType); + J.Annotation annotation = new J.Annotation( + Tree.randomId(), + Space.EMPTY, + Markers.EMPTY, + new J.Identifier(Tree.randomId(), Space.EMPTY, Markers.EMPTY, List.of(), at.getClassName(), at, null), + null); + annotations.add(autoFormat(annotation, ctx)); + m = m.withLeadingAnnotations(annotations); + m = autoFormat(m, m.getName(), ctx, getCursor().getParent()); + maybeAddImport(annotationType); + optAnnotation = Optional.of(annotation); + } + if (attributes != null) { + for (Attribute attr : attributes) { + m = (MethodDeclaration) new AddOrUpdateAnnotationAttribute(annotationType, attr.name(), + attr.value(), true, false).getVisitor().visit(m, ctx, getCursor().getParent()); + } + } + + } + return m; + } + }); + } + + +} diff --git a/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/AddAnnotationOverMethodTest.java b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/AddAnnotationOverMethodTest.java new file mode 100644 index 000000000..f40c94b40 --- /dev/null +++ b/headless-services/commons/commons-rewrite/src/test/java/org/springframework/ide/vscode/commons/rewrite/java/AddAnnotationOverMethodTest.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2025 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.commons.rewrite.java; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.openrewrite.java.Assertions; +import org.openrewrite.test.RewriteTest; + +public class AddAnnotationOverMethodTest implements RewriteTest { + + @Test + void addAnnotationToMethod() { + rewriteRun( + spec -> spec.recipe(new AddAnnotationOverMethod("demo.A foo()", "java.lang.Deprecated", List.of(new AddAnnotationOverMethod.Attribute("value", "\"Expected Text\"")))), + Assertions.java( + """ + package demo; + interface A { + void foo(); + } + """, + """ + package demo; + interface A { + @Deprecated("Expected Text") + void foo(); + } + """ + ) + ); + } + + @Test + void addAnnotationToMethodWithAnnotation() { + rewriteRun( + spec -> spec.recipe(new AddAnnotationOverMethod("demo.A foo()", "java.lang.Deprecated", List.of(new AddAnnotationOverMethod.Attribute("value", "\"Expected Text\"")))), + Assertions.java( + """ + package demo; + class A { + @SuppressWarnings("null") + void foo() { + System.out.println("foo"); + } + } + """, + """ + package demo; + class A { + @SuppressWarnings("null") + @Deprecated("Expected Text") + void foo() { + System.out.println("foo"); + } + } + """ + ) + ); + } + +} 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 e0b00badd..1cd8ed40c 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 @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.app; +import java.util.Collection; import java.util.Optional; import org.springframework.beans.factory.annotation.Qualifier; @@ -17,6 +18,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex; +import org.springframework.ide.vscode.boot.java.codeaction.JdtAstCodeActionProvider; +import org.springframework.ide.vscode.boot.java.codeaction.JdtCodeActionHandler; import org.springframework.ide.vscode.boot.java.cron.CronExpressionsInlayHintsProvider; import org.springframework.ide.vscode.boot.java.cron.CronReconciler; import org.springframework.ide.vscode.boot.java.cron.CronSemanticTokens; @@ -56,6 +59,7 @@ import org.springframework.ide.vscode.boot.java.spel.JdtSpelReconciler; import org.springframework.ide.vscode.boot.java.spel.JdtSpelSemanticTokensProvider; import org.springframework.ide.vscode.boot.java.spel.SpelReconciler; import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens; +import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.languageserver.util.LspClient; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; @@ -191,4 +195,7 @@ public class JdtConfig { return new JdtCronReconciler(cronReconciler); } + @Bean JdtCodeActionHandler jdtCodeActionHandler(CompilationUnitCache cuCache, Collection providers) { + return new JdtCodeActionHandler(cuCache, providers); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java index 6f28d6ac8..6d94fdb08 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/RewriteConfig.java @@ -13,12 +13,13 @@ package org.springframework.ide.vscode.boot.app; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.ide.vscode.boot.java.data.DataRepositoryAotMetadataService; +import org.springframework.ide.vscode.boot.java.data.QueryMethodCodeActionProvider; import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; -import org.springframework.ide.vscode.boot.java.rewrite.RewriteCodeActionHandler; +import org.springframework.ide.vscode.boot.java.reconcilers.ReconcileProblemCodeActionProvider; import org.springframework.ide.vscode.boot.java.rewrite.RewriteRecipeRepository; import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; import org.springframework.ide.vscode.boot.java.rewrite.SpringBootUpgrade; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer; @@ -35,13 +36,18 @@ public class RewriteConfig { } @ConditionalOnBean(RewriteRecipeRepository.class) - @Bean RewriteCodeActionHandler rewriteCodeActionHandler(CompilationUnitCache cuCache, BootJavaConfig config, JdtReconciler jdtReconciler, SimpleLanguageServer server) { - return new RewriteCodeActionHandler(cuCache, config, jdtReconciler, server.getQuickfixRegistry(), server.getDiagnosticSeverityProvider()); + @Bean ReconcileProblemCodeActionProvider reconcileProblemCodeActionProvider(JdtReconciler reconciler, SimpleLanguageServer server) { + return new ReconcileProblemCodeActionProvider(reconciler, server.getDiagnosticSeverityProvider()); } @ConditionalOnBean(RewriteRecipeRepository.class) @Bean SpringBootUpgrade springBootUpgrade(SimpleLanguageServer server, RewriteRecipeRepository recipeRepo, JavaProjectFinder projectFinder) { return new SpringBootUpgrade(server, recipeRepo, projectFinder); } + + @ConditionalOnBean(RewriteRefactorings.class) + @Bean QueryMethodCodeActionProvider queryMethodCodeActionProvider(DataRepositoryAotMetadataService dataRepoAotService, RewriteRefactorings refactorings) { + return new QueryMethodCodeActionProvider(dataRepoAotService, refactorings); + } } 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 eb38e2b9d..f8c78177d 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 @@ -62,6 +62,7 @@ import org.springframework.ide.vscode.boot.java.requestmapping.LiveAppURLSymbolP import org.springframework.ide.vscode.boot.java.requestmapping.RequestMappingHoverProvider; import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxHandlerCodeLensProvider; import org.springframework.ide.vscode.boot.java.requestmapping.WebfluxRouteHighlightProdivder; +import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; import org.springframework.ide.vscode.boot.java.spel.SpelSemanticTokens; import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; import org.springframework.ide.vscode.boot.java.value.ValueHoverProvider; @@ -181,7 +182,7 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent spelSemanticTokens = appContext.getBean(SpelSemanticTokens.class); dataRepositoryAotMetadataService = appContext.getBean(DataRepositoryAotMetadataService.class); - codeLensHandler = createCodeLensEngine(springIndex, projectFinder, server, spelSemanticTokens, dataRepositoryAotMetadataService); + codeLensHandler = createCodeLensEngine(springIndex, projectFinder, server, spelSemanticTokens, dataRepositoryAotMetadataService, appContext.getBean(RewriteRefactorings.class)); highlightsEngine = createDocumentHighlightEngine(appContext); documents.onDocumentHighlight(highlightsEngine); @@ -316,12 +317,12 @@ public class BootJavaLanguageServerComponents implements LanguageServerComponent } protected BootJavaCodeLensEngine createCodeLensEngine(SpringMetamodelIndex springIndex, JavaProjectFinder projectFinder, SimpleLanguageServer server, - SpelSemanticTokens spelSemanticTokens, DataRepositoryAotMetadataService repositoryAotMetadataService) { + SpelSemanticTokens spelSemanticTokens, DataRepositoryAotMetadataService repositoryAotMetadataService, RewriteRefactorings refactorings) { Collection codeLensProvider = new ArrayList<>(); codeLensProvider.add(new WebfluxHandlerCodeLensProvider(springIndex)); codeLensProvider.add(new CopilotCodeLensProvider(projectFinder, server, spelSemanticTokens)); - codeLensProvider.add(new DataRepositoryAotMetadataCodeLensProvider(projectFinder, repositoryAotMetadataService)); + codeLensProvider.add(new DataRepositoryAotMetadataCodeLensProvider(projectFinder, repositoryAotMetadataService, refactorings)); return new BootJavaCodeLensEngine(this, codeLensProvider); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/codeaction/JdtAstCodeActionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/codeaction/JdtAstCodeActionProvider.java new file mode 100644 index 000000000..58536c66a --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/codeaction/JdtAstCodeActionProvider.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2025 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.codeaction; + +import java.net.URI; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ICollector; +import org.springframework.ide.vscode.commons.util.text.IRegion; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public interface JdtAstCodeActionProvider { + + boolean isApplicable(IJavaProject project); + + ASTVisitor createVisitor(CancelChecker cancelToken, IJavaProject project, URI docURI, CompilationUnit cu, TextDocument doc, IRegion region, ICollector collector); + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/codeaction/JdtCodeActionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/codeaction/JdtCodeActionHandler.java new file mode 100644 index 000000000..24352cbaf --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/codeaction/JdtCodeActionHandler.java @@ -0,0 +1,126 @@ +/******************************************************************************* + * Copyright (c) 2025 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.codeaction; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionCapabilities; +import org.eclipse.lsp4j.CodeActionContext; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.CodeActionResolveSupportCapabilities; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.eclipse.lsp4j.jsonrpc.messages.Either; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.ide.vscode.boot.java.handlers.JavaCodeActionHandler; +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.reconcile.BasicCollector; +import org.springframework.ide.vscode.commons.languageserver.util.LspClient; +import org.springframework.ide.vscode.commons.languageserver.util.LspClient.Client; +import org.springframework.ide.vscode.commons.util.text.IRegion; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class JdtCodeActionHandler implements JavaCodeActionHandler { + + private static Logger log = LoggerFactory.getLogger(JdtCodeActionHandler.class); + + final private CompilationUnitCache cuCache; + final private Collection providers; + + public JdtCodeActionHandler(CompilationUnitCache cuCache, Collection providers) { + this.cuCache = cuCache; + this.providers = providers; + } + + protected static boolean isResolve(CodeActionCapabilities capabilities, String property) { + if (capabilities != null) { + CodeActionResolveSupportCapabilities resolveSupport = capabilities.getResolveSupport(); + if (resolveSupport != null) { + List properties = resolveSupport.getProperties(); + return properties != null && properties.contains(property); + } + } + return false; + } + + private boolean isSupported(CodeActionCapabilities capabilities, CodeActionContext context) { + // Default case is anything non-quick fix related. + if (isResolve(capabilities, "edit")) { + if (context.getOnly() != null) { + return context.getOnly().contains(CodeActionKind.Refactor); + } else { + if (LspClient.currentClient() == Client.ECLIPSE) { + // Eclipse would have no diagnostics in the context for QuickAssists refactoring. Diagnostics will be around for QuickFix only + return context.getDiagnostics().isEmpty(); + } else { + return true; + } + + } + } + return false; + } + + @Override + public List> handle(IJavaProject project, CancelChecker cancelToken, + CodeActionCapabilities capabilities, CodeActionContext context, TextDocument doc, IRegion region) { + try { + + URI uri = URI.create(doc.getUri()); + + if (isSupported(capabilities, context)) { + + List applicableProviders = providers.stream().filter(p -> p.isApplicable(project)).toList(); + if (!applicableProviders.isEmpty()) { + List codeActions = cuCache.withCompilationUnit(project, uri, cu -> { + if (cu != null) { + try { + List cas = new ArrayList<>(); + BasicCollector codeActionsCollector = new BasicCollector<>(cas); + + CompositeASTVisitor v = new CompositeASTVisitor(); + for (JdtAstCodeActionProvider p : applicableProviders) { + v.add(p.createVisitor(cancelToken, project, uri, cu, doc, region, codeActionsCollector)); + } + + codeActionsCollector.beginCollecting(); + + cu.accept(v); + + codeActionsCollector.endCollecting(); + + return cas; + } catch (Exception e) { + log.error("", e); + } + } + return Collections.emptyList(); + }); + + return codeActions.stream().map(ca -> Either.forRight(ca)).collect(Collectors.toList()); + } + } + } catch (Exception 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/DataRepositoryAotMetadataCodeLensProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataCodeLensProvider.java index b90d4db1b..80277e7ca 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataCodeLensProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryAotMetadataCodeLensProvider.java @@ -1,17 +1,22 @@ /******************************************************************************* - * Copyright (c) 2025 Broadcom + * Copyright (c) 2025 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 - initial API and implementation + * Broadcom, Inc. - initial API and implementation *******************************************************************************/ package org.springframework.ide.vscode.boot.java.data; +import java.util.Arrays; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.text.StringEscapeUtils; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.IAnnotationBinding; @@ -25,8 +30,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.Annotations; import org.springframework.ide.vscode.boot.java.handlers.CodeLensProvider; +import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder; +import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope; +import org.springframework.ide.vscode.commons.rewrite.java.AddAnnotationOverMethod; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; import org.springframework.ide.vscode.commons.util.BadLocationException; import org.springframework.ide.vscode.commons.util.text.TextDocument; @@ -39,10 +48,12 @@ public class DataRepositoryAotMetadataCodeLensProvider implements CodeLensProvid private final DataRepositoryAotMetadataService repositoryMetadataService; private final JavaProjectFinder projectFinder; + private final RewriteRefactorings refactorings; - public DataRepositoryAotMetadataCodeLensProvider(JavaProjectFinder projectFinder, DataRepositoryAotMetadataService repositoryMetadataService) { + public DataRepositoryAotMetadataCodeLensProvider(JavaProjectFinder projectFinder, DataRepositoryAotMetadataService repositoryMetadataService, RewriteRefactorings refactorings) { this.projectFinder = projectFinder; this.repositoryMetadataService = repositoryMetadataService; + this.refactorings = refactorings; } @Override @@ -55,59 +66,65 @@ public class DataRepositoryAotMetadataCodeLensProvider implements CodeLensProvid } }); } - - protected void provideCodeLens(CancelChecker cancelToken, MethodDeclaration node, TextDocument document, List resultAccumulator) { - cancelToken.checkCanceled(); - - IMethodBinding methodBinding = node.resolveBinding(); + + static boolean isDataQuaryNonAnnotatedMethodCandidate(IMethodBinding methodBinding) { + if (methodBinding == null || methodBinding.getDeclaringClass() == null + || methodBinding.getMethodDeclaration() == null + || methodBinding.getDeclaringClass().getBinaryName() == null + || methodBinding.getMethodDeclaration().toString() == null) { + return false; + } // Don't show CodeLens if annotated with `@Query` or `@NativeQuery` for (IAnnotationBinding a : methodBinding.getAnnotations()) { ITypeBinding t = a.getAnnotationType(); if (t != null && (Annotations.DATA_JPA_QUERY.equals(t.getQualifiedName()) || Annotations.DATA_JPA_NATIVE_QUERY.equals(t.getQualifiedName()))) { - return; + return false; } } - if (methodBinding == null || methodBinding.getDeclaringClass() == null - || methodBinding.getMethodDeclaration() == null - || methodBinding.getDeclaringClass().getBinaryName() == null - || methodBinding.getMethodDeclaration().toString() == null) { - return; - } - - cancelToken.checkCanceled(); - + return true; + } + + static Optional getDataQuery(DataRepositoryAotMetadataService repositoryMetadataService, IJavaProject project, IMethodBinding methodBinding) { final String repositoryClass = methodBinding.getDeclaringClass().getBinaryName().trim(); final IMethodBinding method = methodBinding.getMethodDeclaration(); + DataRepositoryAotMetadata metadata = repositoryMetadataService.getRepositoryMetadata(project, repositoryClass); + + if (metadata != null) { + return Optional.ofNullable(repositoryMetadataService.getQueryStatement(metadata, method)); + } + + return Optional.empty(); + + } + + protected void provideCodeLens(CancelChecker cancelToken, MethodDeclaration node, TextDocument document, List resultAccumulator) { + cancelToken.checkCanceled(); + IJavaProject project = projectFinder.find(document.getId()).get(); if (project == null) { return; } - DataRepositoryAotMetadata metadata = repositoryMetadataService.getRepositoryMetadata(project, repositoryClass); - - if (metadata == null) { - return; + IMethodBinding methodBinding = node.resolveBinding(); + + if (isDataQuaryNonAnnotatedMethodCandidate(methodBinding)) { + cancelToken.checkCanceled(); + getDataQuery(repositoryMetadataService, project, methodBinding).map(queryStatement -> createCodeLens(node, document, queryStatement)).ifPresent(resultAccumulator::add); } - - cancelToken.checkCanceled(); - - String queryStatement = repositoryMetadataService.getQueryStatement(metadata, method); - if (queryStatement == null) { - return; - } - - CodeLens codeLens = createCodeLens(node, document, queryStatement); - resultAccumulator.add(codeLens); } private CodeLens createCodeLens(MethodDeclaration node, TextDocument document, String queryStatement) { try { + IMethodBinding mb = node.resolveBinding(); Command cmd = new Command(); cmd.setTitle(queryStatement); + if (mb != null) { + cmd = refactorings.createFixCommand(queryStatement, createFixDescriptor(mb, document.getUri(), queryStatement)); + } CodeLens codeLens = new CodeLens(); codeLens.setRange(document.toRange(node.getName().getStartPosition(), node.getName().getLength())); @@ -120,5 +137,16 @@ public class DataRepositoryAotMetadataCodeLensProvider implements CodeLensProvid return null; } } + + static FixDescriptor createFixDescriptor(IMethodBinding mb, String docUri, String queryStatement) { + return new FixDescriptor(AddAnnotationOverMethod.class.getName(), List.of(docUri), "Convert into `@Query`") + .withRecipeScope(RecipeScope.FILE) + .withParameters(Map.of("annotationType", Annotations.DATA_JPA_QUERY, "method", + "%s %s(%s)".formatted(mb.getDeclaringClass().getQualifiedName(), mb.getName(), + Arrays.stream(mb.getParameterTypes()).map(pt -> pt.getQualifiedName()) + .collect(Collectors.joining(", "))), + "attributes", List.of(new AddAnnotationOverMethod.Attribute("value", + "\"%s\"".formatted(StringEscapeUtils.escapeJava(queryStatement)))))); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java new file mode 100644 index 000000000..1ec12a9eb --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/QueryMethodCodeActionProvider.java @@ -0,0 +1,83 @@ +/******************************************************************************* + * Copyright (c) 2025 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; + +import java.net.URI; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.boot.java.codeaction.JdtAstCodeActionProvider; +import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; +import org.springframework.ide.vscode.commons.Version; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.java.SpringProjectUtil; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ICollector; +import org.springframework.ide.vscode.commons.util.text.IRegion; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class QueryMethodCodeActionProvider implements JdtAstCodeActionProvider { + + private static final String TITLE = "Convert into `@Query`"; + + private final DataRepositoryAotMetadataService repositoryMetadataService; + private final RewriteRefactorings refactorings; + + public QueryMethodCodeActionProvider(DataRepositoryAotMetadataService repositoryMetadataService, RewriteRefactorings refactorings) { + this.repositoryMetadataService = repositoryMetadataService; + this.refactorings = refactorings; + } + + @Override + public boolean isApplicable(IJavaProject project) { + Version version = SpringProjectUtil.getDependencyVersion(project, "spring-data-jpa"); + return version != null && version.getMajor() >= 4; + } + + @Override + public ASTVisitor createVisitor(CancelChecker cancelToken, IJavaProject project, URI docURI, CompilationUnit cu, TextDocument doc, + IRegion region, ICollector collector) { + return new ASTVisitor() { + + @Override + public boolean visit(MethodDeclaration node) { + cancelToken.checkCanceled(); + if (node.getStartPosition() <= region.getStart() && node.getStartPosition() + node.getLength() >= region.getEnd()) { + int offset = node.getName().getStartPosition(); + int length = node.getName().getLength(); + if (offset <= region.getStart() && offset + length >= region.getEnd()) { + IMethodBinding binding = node.resolveBinding(); + if (DataRepositoryAotMetadataCodeLensProvider.isDataQuaryNonAnnotatedMethodCandidate(binding)) { + DataRepositoryAotMetadataCodeLensProvider.getDataQuery(repositoryMetadataService, project, binding) + .map(query -> createCodeAction(binding, docURI, query)).ifPresent(collector::accept); + } + } + return super.visit(node); + } + return false; + } + + }; + } + + private CodeAction createCodeAction(IMethodBinding mb, URI docUri, String query) { + CodeAction ca = new CodeAction(); + ca.setCommand(refactorings.createFixCommand(TITLE, DataRepositoryAotMetadataCodeLensProvider.createFixDescriptor(mb, docUri.toASCIIString(), query))); + ca.setTitle(TITLE); + ca.setKind(CodeActionKind.Refactor); + return ca; + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java index 69dd9d1c6..80541f08b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/JdtReconciler.java @@ -50,8 +50,8 @@ public class JdtReconciler implements JavaReconciler { public static final String SPRING_CONDITIONAL_ON_EXPRESSION = "org.springframework.boot.autoconfigure.condition.ConditionalOnExpression"; private final CompilationUnitCache compilationUnitCache; - private final JdtAstReconciler[] reconcilers; - private BootJavaConfig config; + final JdtAstReconciler[] reconcilers; + final BootJavaConfig config; private final ConcurrentHashMap> applicableReconcilersCache; diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ReconcileProblemCodeActionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ReconcileProblemCodeActionProvider.java new file mode 100644 index 000000000..bced3792f --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/reconcilers/ReconcileProblemCodeActionProvider.java @@ -0,0 +1,99 @@ +/******************************************************************************* + * Copyright (c) 2025 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.reconcilers; + +import java.net.URI; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.CodeActionKind; +import org.eclipse.lsp4j.jsonrpc.CancelChecker; +import org.springframework.ide.vscode.boot.java.codeaction.JdtAstCodeActionProvider; +import org.springframework.ide.vscode.commons.java.IJavaProject; +import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData; +import org.springframework.ide.vscode.commons.languageserver.reconcile.DiagnosticSeverityProvider; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ICollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; +import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; +import org.springframework.ide.vscode.commons.util.text.IRegion; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +/** + * Ignored (i.e. not shown) reconcile problems with QuickFixes are being shown as CodeActions(s) with this provider. + * Note: Only Rewrite based QuickFixes are supported at the moment hence should be around only if Rewrite recipes are supported + */ +public class ReconcileProblemCodeActionProvider implements JdtAstCodeActionProvider { + + private final JdtReconciler reconciler; + private final DiagnosticSeverityProvider severityProvider; + + + public ReconcileProblemCodeActionProvider(JdtReconciler reconciler, DiagnosticSeverityProvider severityProvider) { + this.reconciler = reconciler; + this.severityProvider = severityProvider; + } + + @Override + public boolean isApplicable(IJavaProject project) { + if (reconciler.config.isJavaSourceReconcileEnabled()) { + return Arrays.stream(reconciler.reconcilers).anyMatch(r -> r.isApplicable(project)); + } + return false; + } + + @Override + public ASTVisitor createVisitor(CancelChecker cancelToken, IJavaProject project, URI docURI, CompilationUnit cu, TextDocument doc, + IRegion region, ICollector collector) { + IProblemCollector problemCollector = new IProblemCollector() { + + @Override + public void endCollecting() { + } + + @Override + public void beginCollecting() { + } + + @Override + public void accept(ReconcileProblem p) { + if (p.getOffset() <= region.getOffset() && p.getOffset() + p.getLength() >= region.getOffset() + region.getLength() && severityProvider.getDiagnosticSeverity(p) == null) { + for (QuickfixData qf : p.getQuickfixes()) { + if (qf.params instanceof FixDescriptor) { + collector.accept(createCodeActionFromScope((FixDescriptor) qf.params)); + } + } + } + } + }; + CompositeASTVisitor v = new CompositeASTVisitor(); + ReconcilingContext ctx = new ReconcilingContext(docURI.toASCIIString(), problemCollector, true, true, List.of()); + for (JdtAstReconciler r : reconciler.reconcilers) { + if (r.isApplicable(project) && severityProvider.getDiagnosticSeverity(r.getProblemType()) == null) { + v.add(r.createVisitor(project, docURI, cu, ctx)); + } + } + return v; + } + + private CodeAction createCodeActionFromScope(FixDescriptor d) { + CodeAction ca = new CodeAction(); + ca.setKind(CodeActionKind.Refactor); + ca.setTitle(d.getLabel()); + ca.setData(d); + return ca; + } + + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteCodeActionHandler.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteCodeActionHandler.java deleted file mode 100644 index 0262ae7fc..000000000 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteCodeActionHandler.java +++ /dev/null @@ -1,157 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2022, 2025 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 - * https://www.eclipse.org/legal/epl-v10.html - * - * Contributors: - * VMware, Inc. - initial API and implementation - *******************************************************************************/ -package org.springframework.ide.vscode.boot.java.rewrite; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -import org.eclipse.lsp4j.CodeAction; -import org.eclipse.lsp4j.CodeActionCapabilities; -import org.eclipse.lsp4j.CodeActionContext; -import org.eclipse.lsp4j.CodeActionKind; -import org.eclipse.lsp4j.CodeActionResolveSupportCapabilities; -import org.eclipse.lsp4j.Command; -import org.eclipse.lsp4j.jsonrpc.CancelChecker; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.ide.vscode.boot.app.BootJavaConfig; -import org.springframework.ide.vscode.boot.java.handlers.JavaCodeActionHandler; -import org.springframework.ide.vscode.boot.java.reconcilers.JdtReconciler; -import org.springframework.ide.vscode.boot.java.reconcilers.ReconcilingContext; -import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache; -import org.springframework.ide.vscode.commons.java.IJavaProject; -import org.springframework.ide.vscode.commons.languageserver.quickfix.Quickfix.QuickfixData; -import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixRegistry; -import org.springframework.ide.vscode.commons.languageserver.quickfix.QuickfixType; -import org.springframework.ide.vscode.commons.languageserver.reconcile.BasicProblemCollector; -import org.springframework.ide.vscode.commons.languageserver.reconcile.DiagnosticSeverityProvider; -import org.springframework.ide.vscode.commons.languageserver.reconcile.ReconcileProblem; -import org.springframework.ide.vscode.commons.languageserver.util.LspClient; -import org.springframework.ide.vscode.commons.languageserver.util.LspClient.Client; -import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor; -import org.springframework.ide.vscode.commons.util.text.IRegion; -import org.springframework.ide.vscode.commons.util.text.TextDocument; - -public class RewriteCodeActionHandler implements JavaCodeActionHandler { - - private static final Logger log = LoggerFactory.getLogger(RewriteCodeActionHandler.class); - - final private CompilationUnitCache cuCache; - final private JdtReconciler jdtReconciler; - - private BootJavaConfig config; - - private QuickfixRegistry quickfixRegistry; - - private DiagnosticSeverityProvider severityProvider; - - public RewriteCodeActionHandler(CompilationUnitCache cuCache, BootJavaConfig config, JdtReconciler jdtReconciler, QuickfixRegistry quickfixRegistry, DiagnosticSeverityProvider severityProvider) { - this.cuCache = cuCache; - this.config = config; - this.jdtReconciler = jdtReconciler; - this.quickfixRegistry = quickfixRegistry; - this.severityProvider = severityProvider; - } - - protected static boolean isResolve(CodeActionCapabilities capabilities, String property) { - if (capabilities != null) { - CodeActionResolveSupportCapabilities resolveSupport = capabilities.getResolveSupport(); - if (resolveSupport != null) { - List properties = resolveSupport.getProperties(); - return properties != null && properties.contains(property); - } - } - return false; - } - - private boolean isSupported(CodeActionCapabilities capabilities, CodeActionContext context) { - // Default case is anything non-quick fix related. - if (isResolve(capabilities, "edit")) { - if (context.getOnly() != null) { - return context.getOnly().contains(CodeActionKind.Refactor); - } else { - if (LspClient.currentClient() == Client.ECLIPSE) { - // Eclipse would have no diagnostics in the context for QuickAssists refactoring. Diagnostics will be around for QuickFix only - return context.getDiagnostics().isEmpty(); - } else { - return true; - } - - } - } - return false; - } - - @Override - public List> handle(IJavaProject project, CancelChecker cancelToken, - CodeActionCapabilities capabilities, CodeActionContext context, TextDocument doc, IRegion region) { - // Short circuit here to avoid parsing the java source for nothing. - if (!config.isJavaSourceReconcileEnabled()) { - return Collections.emptyList(); - } - - try { - - URI uri = URI.create(doc.getUri()); - final QuickfixType rewriteFixType = quickfixRegistry.getQuickfixType(RewriteRefactorings.REWRITE_RECIPE_QUICKFIX); - - if (isSupported(capabilities, context) && rewriteFixType != null) { - List codeActions = cuCache.withCompilationUnit(project, uri, cu -> { - - if (cu != null) { - - try { - List cas = new ArrayList<>(); - List problems = new ArrayList<>(); - BasicProblemCollector problemsCollector = new BasicProblemCollector(problems); - - ReconcilingContext reconcilingContext = new ReconcilingContext(doc.getUri(), problemsCollector, true, true, Collections.emptyList()); - jdtReconciler.reconcile(project, uri, cu, reconcilingContext); - - for (ReconcileProblem p : problems) { - if (p.getOffset() <= region.getOffset() && p.getOffset() + p.getLength() >= region.getOffset() + region.getLength() && severityProvider.getDiagnosticSeverity(p) == null) { - for (QuickfixData qf : p.getQuickfixes()) { - if (qf.params instanceof FixDescriptor) { - cas.add(createCodeActionFromScope((FixDescriptor) qf.params)); - } - } - } - } - return cas; - - } catch (Exception e) { - log.error("", e); - } - } - return Collections.emptyList(); - }); - - return codeActions.stream().map(ca -> Either.forRight(ca)).collect(Collectors.toList()); - } - } catch (Exception e) { - log.error("", e); - } - return Collections.emptyList(); - } - - private CodeAction createCodeActionFromScope(FixDescriptor d) { - CodeAction ca = new CodeAction(); - ca.setKind(CodeActionKind.Refactor); - ca.setTitle(d.getLabel()); - ca.setData(d); - return ca; - } - -} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java index e68f5fbad..1cfc65159 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/rewrite/RewriteRefactorings.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022, 2024 VMware, Inc. + * Copyright (c) 2022, 2025 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 @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.boot.java.rewrite; import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; import java.net.URI; import java.nio.file.Paths; import java.util.ArrayList; @@ -213,7 +214,13 @@ public class RewriteRefactorings implements CodeActionResolver, QuickfixHandler Field field = findField(recipe, entry.getKey()); if (field != null) { field.setAccessible(true); - field.set(recipe, entry.getValue()); + if (!field.getType().isAssignableFrom(entry.getValue().getClass())) { + field.set(recipe, gson.fromJson(gson.toJsonTree(entry.getValue()), field.getType())); + } else if (field.getGenericType() instanceof ParameterizedType pt) { + field.set(recipe, gson.fromJson(gson.toJsonTree(entry.getValue()), pt)); + } else { + field.set(recipe, entry.getValue()); + } } } catch (Exception e) { log.error("", e);; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java index 9e284399b..21a430169 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryAotMetadataCodeLensProviderTest.java @@ -1,3 +1,13 @@ +/******************************************************************************* + * Copyright (c) 2025 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.test; import java.nio.charset.StandardCharsets; diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/QueryMethodCodeActionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/QueryMethodCodeActionProviderTest.java new file mode 100644 index 000000000..f210e86ef --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/QueryMethodCodeActionProviderTest.java @@ -0,0 +1,104 @@ +/******************************************************************************* + * Copyright (c) 2025 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.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.TextDocumentEdit; +import org.eclipse.lsp4j.TextDocumentIdentifier; +import org.eclipse.lsp4j.WorkspaceEdit; +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.app.SpringSymbolIndex; +import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest; +import org.springframework.ide.vscode.boot.bootiful.SymbolProviderTestConf; +import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings; +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.LanguageId; +import org.springframework.ide.vscode.languageserver.testharness.CodeAction; +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; + +import com.google.gson.JsonElement; + +@ExtendWith(SpringExtension.class) +@BootLanguageServerTest +@Import(SymbolProviderTestConf.class) +public class QueryMethodCodeActionProviderTest { + + @Autowired private BootLanguageServerHarness harness; + @Autowired private JavaProjectFinder projectFinder; + @Autowired private SpringSymbolIndex indexer; + @Autowired private RewriteRefactorings refactorings; + + private IJavaProject testProject; + + @BeforeEach + public void setup() throws Exception { + testProject = ProjectsHarness.INSTANCE.mavenProject("aot-generation"); + harness.useProject(testProject); + harness.intialize(null); + + // trigger project creation + projectFinder.find(new TextDocumentIdentifier(testProject.getLocationUri().toASCIIString())).get(); + + CompletableFuture initProject = indexer.waitOperation(); + initProject.get(5, TimeUnit.SECONDS); + } + + @Test + void convertToQueryCodeAction() throws Exception { + Path filePath = Paths.get(testProject.getLocationUri()) + .resolve("src/main/java/example/springdata/aot/UserRepository.java"); + Editor editor = harness.newEditor(LanguageId.JAVA, + new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString()); + + List codeActions = editor.getCodeActions("findUserByLastnameStartingWith", 1); + assertEquals(1, codeActions.size()); + CodeAction ca = codeActions.get(0); + assertEquals("Convert into `@Query`", ca.getLabel()); + Command cmd = ca.getCommand(); + assertEquals(RewriteRefactorings.REWRITE_RECIPE_QUICKFIX, cmd.getArguments().get(0)); + WorkspaceEdit edit = refactorings.createEdit((JsonElement) cmd.getArguments().get(1)).get(1, TimeUnit.SECONDS); + TextDocumentEdit docEdit = edit.getDocumentChanges().get(0).getLeft(); + assertEquals( + "@Query(\"SELECT u FROM example.springdata.aot.User u WHERE u.lastname LIKE :lastname ESCAPE '\\\\' ORDER BY u.firstname asc\")", + docEdit.getEdits().get(0).getNewText().trim()); + assertEquals(filePath.toUri().toASCIIString(), docEdit.getTextDocument().getUri()); + } + + @Test + void noConvertToQueryCodeAction() throws Exception { + Path filePath = Paths.get(testProject.getLocationUri()) + .resolve("src/main/java/example/springdata/aot/UserRepository.java"); + Editor editor = harness.newEditor(LanguageId.JAVA, + new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8), filePath.toUri().toASCIIString()); + + List codeActions = editor.getCodeActions("usersWithUsernamesStartingWith", 1); + assertEquals(0, codeActions.size()); + } + +}