diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java index 36bfa7fdd..eb9c1bc0b 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java @@ -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()))); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java index 43f1a7c2a..304c3a2dd 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/annotations/AnnotationAttributeCompletionProcessor.java @@ -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 candidates = completionProvider.getCompletionCandidates(project); List 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()); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionalonresource/ConditionalOnResourceCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionalonresource/ConditionalOnResourceCompletionProcessor.java index 47bd4b28f..b18d8760e 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionalonresource/ConditionalOnResourceCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/conditionalonresource/ConditionalOnResourceCompletionProcessor.java @@ -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 completions) { - - try { - Optional 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 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 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 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 findResources(IJavaProject project) { + List resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream() .distinct() .sorted(new Comparator() { @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 getCompletionCandidates(IJavaProject project) { + return findResources(project); + } + } \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java index fad38e2ee..ce2697b4d 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java @@ -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\"<*>)"); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonresource/test/ConditionalOnResourceCompletionTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonresource/test/ConditionalOnResourceCompletionTest.java index dbddafff9..013ffa726 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonresource/test/ConditionalOnResourceCompletionTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/conditionalonresource/test/ConditionalOnResourceCompletionTest.java @@ -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());