GH-1344: refactored conditional on resource completion processor to be based on annotation attribute completion processor abstraction

This commit is contained in:
Martin Lippert
2024-09-05 12:13:14 +02:00
parent c1970b7ed5
commit 565904de19
5 changed files with 39 additions and 142 deletions

View File

@@ -119,7 +119,7 @@ public class BootJavaCompletionEngineConfigurer {
providers.put(Annotations.VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocProperties));
providers.put(Annotations.CONTEXT_CONFIGURATION, new ContextConfigurationProcessor(javaProjectFinder));
providers.put(Annotations.CONDITIONAL_ON_RESOURCE, new ConditionalOnResourceCompletionProcessor(javaProjectFinder));
providers.put(Annotations.CONDITIONAL_ON_RESOURCE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("resources", new ConditionalOnResourceCompletionProcessor())));
providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor());
providers.put(Annotations.SCOPE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new ScopeCompletionProcessor())));

View File

@@ -24,6 +24,8 @@ import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ArrayInitializer;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
@@ -59,17 +61,22 @@ public class AnnotationAttributeCompletionProcessor implements CompletionProvide
try {
// in case the node is embedded in an qualified name, e.g. "file.txt", use the fully qualified node instead just a part
if (node instanceof Name && node.getParent() instanceof QualifiedName) {
node = node.getParent();
}
// case: @Qualifier(<*>)
if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) {
createCompletionProposals(project, doc, node, "value", completions, offset, offset, "", (beanName) -> "\"" + beanName + "\"");
}
// case: @Qualifier(prefix<*>)
else if (node instanceof SimpleName && node.getParent() instanceof Annotation
else if (node instanceof Name && node.getParent() instanceof Annotation
&& node != annotation.getTypeName()) {
computeProposalsForSimpleName(project, node, "value", completions, offset, doc);
}
// case: @Qualifier(value=<*>)
else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair
else if (node instanceof Name && node.getParent() instanceof MemberValuePair
&& completionProviders.containsKey(((MemberValuePair)node.getParent()).getName().toString())) {
String attributeName = ((MemberValuePair)node.getParent()).getName().toString();
computeProposalsForSimpleName(project, node, attributeName, completions, offset, doc);
@@ -117,7 +124,8 @@ public class AnnotationAttributeCompletionProcessor implements CompletionProvide
List<String> candidates = completionProvider.getCompletionCandidates(project);
List<String> filteredCandidates = candidates.stream()
.filter(candidate -> candidate.toLowerCase().startsWith(filterPrefix.toLowerCase()))
// .filter(candidate -> candidate.toLowerCase().startsWith(filterPrefix.toLowerCase()))
.filter(candidate -> candidate.toLowerCase().contains(filterPrefix.toLowerCase()))
.filter(candidate -> !alreadyMentionedValues.contains(candidate))
.collect(Collectors.toList());

View File

@@ -10,146 +10,21 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.conditionalonresource;
import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseToHyphens;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.Comparator;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.openrewrite.yaml.internal.grammar.JsonPathParser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProposal;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
import org.springframework.ide.vscode.boot.metadata.PropertyInfo;
import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.FuzzyMap;
import org.springframework.ide.vscode.commons.util.FuzzyMap.Match;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
/**
* @author Karthik Sankaranarayanan
*/
public class ConditionalOnResourceCompletionProcessor implements CompletionProvider {
public class ConditionalOnResourceCompletionProcessor implements AnnotationAttributeCompletionProvider {
private static final Logger log = LoggerFactory.getLogger(ConditionalOnResourceCompletionProcessor.class);
private final JavaProjectFinder projectFinder;
public ConditionalOnResourceCompletionProcessor(JavaProjectFinder projectFinder) {
this.projectFinder = projectFinder;
}
@Override
public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type,
int offset, TextDocument doc, Collection<ICompletionProposal> completions) {
try {
Optional<IJavaProject> optionalProject = this.projectFinder.find(doc.getId());
if (optionalProject.isEmpty()) {
return;
}
IJavaProject project = optionalProject.get();
// case: @ConditionalOnResource(resources=<*>)
if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair
&& ("resources".equals(((MemberValuePair)node.getParent()).getName().toString()))) {
computeProposalsForSimpleName(project, node, completions, offset, doc);
}
// case: @ConditionalOnResource(resources=<*>)
else if (node instanceof SimpleName && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof MemberValuePair
&& ("resources".equals(((MemberValuePair)node.getParent().getParent()).getName().toString()))) {
computeProposalsForSimpleName(project, node.getParent(), completions, offset, doc);
}
// case:@ConditionalOnResource(resources="prefix<*>")
else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair
&& ("resources".equals(((MemberValuePair)node.getParent()).getName().toString()))) {
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
computeProposalsForStringLiteral(project, (StringLiteral) node, completions, offset, doc);
}
}
}
catch (Exception e) {
log.error("problem while looking for ConditionalOnResource annotation proposals", e);
}
}
private void addClasspathResourceProposals(IJavaProject project, TextDocument doc, int startOffset, int endOffset, String prefix, boolean includeQuotes, Collection<ICompletionProposal> completions) {
String[] resources = findResources(project, prefix);
double score = resources.length + 1000;
for (String resource : resources) {
DocumentEdits edits = new DocumentEdits(doc, false);
if (includeQuotes) {
edits.replace(startOffset, endOffset, "\"classpath:" + resource + "\"");
}
else {
edits.replace(startOffset, endOffset, "classpath:" + resource);
}
String label = "classpath:" + resource;
ICompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, label, label, null, score--);
completions.add(proposal);
}
}
private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) {
int startOffset = node.getStartPosition();
int endOffset = node.getStartPosition() + node.getLength();
String unfilteredPrefix = node.toString().substring(0, offset - node.getStartPosition());
addClasspathResourceProposals(project, doc, startOffset, endOffset, unfilteredPrefix, true, completions);
}
private void computeProposalsForStringLiteral(IJavaProject project, StringLiteral node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) throws BadLocationException {
String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, offset - (node.getStartPosition() + 1)), offset - (node.getStartPosition() + 1));
int startOffset = offset - prefix.length();
int endOffset = offset;
String unfilteredPrefix = node.getLiteralValue().substring(0, offset - (node.getStartPosition() + 1));
addClasspathResourceProposals(project, doc, startOffset, endOffset, unfilteredPrefix, false, completions);
}
public String identifyPropertyPrefix(String nodeContent, int offset) {
String result = nodeContent.substring(0, offset);
int i = offset - 1;
while (i >= 0) {
char c = nodeContent.charAt(i);
if (c == '}' || c == '{' || c == '$' || c == '#') {
result = result.substring(i + 1, offset);
break;
}
i--;
}
return result;
}
private String[] findResources(IJavaProject project, String prefix) {
String[] resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream()
private List<String> findResources(IJavaProject project) {
List<String> resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream()
.distinct()
.sorted(new Comparator<String>() {
@Override
@@ -158,10 +33,15 @@ public class ConditionalOnResourceCompletionProcessor implements CompletionProvi
}
})
.map(r -> r.replaceAll("\\\\", "/"))
.filter(r -> ("classpath:" + r).contains(prefix))
.toArray(String[]::new);
.map(r -> "classpath:" + r)
.toList();
return resources;
}
@Override
public List<String> getCompletionCandidates(IJavaProject project) {
return findResources(project);
}
}

View File

@@ -103,6 +103,11 @@ public class DependsOnCompletionProviderTest {
assertCompletions("@DependsOn(be<*>)", 2, "@DependsOn(\"bean1\"<*>)");
}
@Test
public void testDependsOnCompletionWithoutQuotesWithNotExactPrefix() throws Exception {
assertCompletions("@DependsOn(ea<*>)", 2, "@DependsOn(\"bean1\"<*>)");
}
@Test
public void testDependsOnCompletionWithoutQuotesWithAttributeName() throws Exception {
assertCompletions("@DependsOn(value=<*>)", 2, "@DependsOn(value=\"bean1\"<*>)");

View File

@@ -181,7 +181,15 @@ public class ConditionalOnResourceCompletionTest {
}
@Test
void testComplexResourceNameInPrefixWithParamNameCompletion() throws Exception {
void testResourceNameInPrefixWithParamNameCompletion() throws Exception {
prepareCase("@ConditionalOnResource(resources=\"onClass\")", "@ConditionalOnResource(resources=root<*>)");
assertClasspathCompletions(
"@ConditionalOnResource(resources=\"classpath:a-random-resource-root.md\"<*>)");
}
@Test
void testQualifiedResourceNameInPrefixWithParamNameCompletion() throws Exception {
prepareCase("@ConditionalOnResource(resources=\"onClass\")", "@ConditionalOnResource(resources=root.md<*>)");
assertClasspathCompletions(
@@ -210,10 +218,6 @@ public class ConditionalOnResourceCompletionTest {
assertClasspathCompletions();
}
private void prepareCase(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception {
InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-annotation-conditionalonresource/src/main/java/org/test/TestConditionalOnResourceCompletion.java");
String content = IOUtils.toString(resource, Charset.defaultCharset());