Command for Data Query method CodeLens and CodeAction

This commit is contained in:
aboyko
2025-04-28 17:10:18 -04:00
parent 8bebac55a8
commit 1fa315f5fe
18 changed files with 801 additions and 222 deletions

View File

@@ -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<T> implements ICollector<T> {
private final Collection<T> collection;
public BasicCollector(Collection<T> collection) {
this.collection = collection;
}
@Override
public void beginCollecting() {
}
@Override
public void endCollecting() {
}
@Override
public void accept(T t) {
collection.add(t);
}
}

View File

@@ -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<T> {
void beginCollecting();
void endCollecting();
void accept(T t);
/**
* Optional for both implementors and callers.
* <p/>
* 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.
* <p/>
* 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() {
}
}

View File

@@ -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.
* <p/>
* 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.
* <p/>
* 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<ReconcileProblem> {
/**
* Problem collector that simply ignores/discards anything passed to it.

View File

@@ -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<Attribute> attributes;
@JsonCreator
public AddAnnotationOverMethod(
@JsonProperty("method") String method,
@JsonProperty("annotationType") String annotationType,
@JsonProperty("attributes") @Nullable List<Attribute> attributes) {
this.method = method;
this.annotationType = annotationType;
this.attributes = attributes;
}
@Override
public TreeVisitor<?, ExecutionContext> 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<Annotation> optAnnotation = m.getLeadingAnnotations().stream().filter(a -> TypeUtils.isOfClassType(a.getType(), annotationType)).findFirst();
if (optAnnotation.isEmpty()) {
List<J.Annotation> 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;
}
});
}
}

View File

@@ -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");
}
}
"""
)
);
}
}

View File

@@ -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<JdtAstCodeActionProvider> providers) {
return new JdtCodeActionHandler(cuCache, providers);
}
}

View File

@@ -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);
}
}

View File

@@ -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> 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);
}

View File

@@ -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<CodeAction> collector);
}

View File

@@ -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<JdtAstCodeActionProvider> providers;
public JdtCodeActionHandler(CompilationUnitCache cuCache, Collection<JdtAstCodeActionProvider> 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<String> 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<Either<Command, CodeAction>> 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<JdtAstCodeActionProvider> applicableProviders = providers.stream().filter(p -> p.isApplicable(project)).toList();
if (!applicableProviders.isEmpty()) {
List<CodeAction> codeActions = cuCache.withCompilationUnit(project, uri, cu -> {
if (cu != null) {
try {
List<CodeAction> cas = new ArrayList<>();
BasicCollector<CodeAction> 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.<Command, CodeAction>forRight(ca)).collect(Collectors.toList());
}
}
} catch (Exception e) {
log.error("", e);
}
return Collections.emptyList();
}
}

View File

@@ -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<CodeLens> 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<String> 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<CodeLens> 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))))));
}
}

View File

@@ -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<CodeAction> 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;
}
}

View File

@@ -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<String, List<JdtAstReconciler>> applicableReconcilersCache;

View File

@@ -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.
* <b>Note:</b> 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<CodeAction> 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;
}
}

View File

@@ -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<String> 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<Either<Command, CodeAction>> 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<CodeAction> codeActions = cuCache.withCompilationUnit(project, uri, cu -> {
if (cu != null) {
try {
List<CodeAction> cas = new ArrayList<>();
List<ReconcileProblem> 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.<Command, CodeAction>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;
}
}

View File

@@ -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);;

View File

@@ -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;

View File

@@ -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<Void> 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<CodeAction> 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<CodeAction> codeActions = editor.getCodeActions("usersWithUsernamesStartingWith", 1);
assertEquals(0, codeActions.size());
}
}