GH-1305: add content-assist for property keys and prefixes for conditional on property annotation

Signed-off-by: Martin Lippert <martin.lippert@broadcom.com>
This commit is contained in:
Martin Lippert
2024-11-04 12:23:30 +01:00
parent 9c367fd738
commit 6e1f8142e2
8 changed files with 466 additions and 6 deletions

View File

@@ -32,6 +32,7 @@ import org.springframework.ide.vscode.boot.java.beans.NamedCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider;
import org.springframework.ide.vscode.boot.java.beans.ResourceCompletionProvider;
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnPropertyCompletionProcessor;
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalOnResourceCompletionProcessor;
import org.springframework.ide.vscode.boot.java.contextconfiguration.ContextConfigurationProcessor;
import org.springframework.ide.vscode.boot.java.cron.CronExpressionCompletionProvider;
@@ -127,6 +128,11 @@ public class BootJavaCompletionEngineConfigurer {
providers.put(Annotations.CONDITIONAL_ON_RESOURCE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of(
"resources", new ConditionalOnResourceCompletionProcessor())));
providers.put(Annotations.CONDITIONAL_ON_PROPERTY, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of(
"value", new ConditionalOnPropertyCompletionProcessor(indexProvider, adHocProperties, ConditionalOnPropertyCompletionProcessor.Mode.PROPERTY),
"name", new ConditionalOnPropertyCompletionProcessor(indexProvider, adHocProperties, ConditionalOnPropertyCompletionProcessor.Mode.PROPERTY),
"prefix", new ConditionalOnPropertyCompletionProcessor(indexProvider, adHocProperties, ConditionalOnPropertyCompletionProcessor.Mode.PREFIX))));
providers.put(Annotations.SCOPE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of(
"value", new ScopeCompletionProcessor())));

View File

@@ -0,0 +1,153 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Broadcom, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.conditionals;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.MemberValuePair;
import org.eclipse.jdt.core.dom.NormalAnnotation;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeProposal;
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
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.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.util.FuzzyMap;
/**
* @author Martin Lippert
*/
public class ConditionalOnPropertyCompletionProcessor implements AnnotationAttributeCompletionProvider {
public enum Mode {
PREFIX, PROPERTY
}
private final SpringPropertyIndexProvider indexProvider;
private final ProjectBasedPropertyIndexProvider adHocIndexProvider;
private final Mode mode;
public ConditionalOnPropertyCompletionProcessor(SpringPropertyIndexProvider indexProvider,
ProjectBasedPropertyIndexProvider adHocIndexProvider,
Mode mode) {
this.indexProvider = indexProvider;
this.adHocIndexProvider = adHocIndexProvider;
this.mode = mode;
}
@Override
public List<AnnotationAttributeProposal> getCompletionCandidates(IJavaProject project, ASTNode node) {
if (Mode.PROPERTY == this.mode) {
String prefix = getPrefixAttributeValue(node);
return findProperties(project, prefix);
}
else if (Mode.PREFIX == this.mode) {
return findPrefixes(project);
}
else {
return Collections.emptyList();
}
}
private List<AnnotationAttributeProposal> findProperties(IJavaProject project, String prefix) {
List<AnnotationAttributeProposal> result = new ArrayList<>();
// First the 'real' properties, Then also add 'ad-hoc' properties
addPropertyProposals(indexProvider.getIndex(project).getProperties(), prefix, result);
addPropertyProposals(adHocIndexProvider.getIndex(project), prefix, result);
result.sort((p1, p2) -> p1.getLabel().compareTo(p2.getLabel()));
return result;
}
private List<AnnotationAttributeProposal> findPrefixes(IJavaProject project) {
Set<AnnotationAttributeProposal> prefixes = new TreeSet<>((p1, p2) -> p1.getLabel().compareTo(p2.getLabel()));
// First the 'real' properties, then also add 'ad-hoc' properties
addPrefixProposals(indexProvider.getIndex(project).getProperties(), prefixes);
addPrefixProposals(adHocIndexProvider.getIndex(project), prefixes);
return new ArrayList<>(prefixes);
}
private void addPropertyProposals(FuzzyMap<PropertyInfo> properties, String prefix, List<AnnotationAttributeProposal> result) {
properties.forEach(propertyInfo -> {
String propID = propertyInfo.getId();
if (prefix != null) {
if (prefix.length() > 0
&& prefix.length() < propID.length()
&& propID.startsWith(prefix)) {
String remainingValue = propID.substring(prefix.length() + 1);
result.add(new AnnotationAttributeProposal(propID, propID, remainingValue));
}
}
else {
result.add(new AnnotationAttributeProposal(propID));
}
});
}
private void addPrefixProposals(FuzzyMap<PropertyInfo> properties, Set<AnnotationAttributeProposal> prefixes) {
properties.forEach(propertyInfo -> {
String prefix = getPrefix(propertyInfo.getId());
while (prefix != null) {
prefixes.add(new AnnotationAttributeProposal(prefix));
prefix = getPrefix(prefix);
}
});
}
private String getPrefix(String key) {
int index = key.lastIndexOf('.');
if (index >= 0) {
return key.substring(0, index);
}
else {
return null;
}
}
private String getPrefixAttributeValue(ASTNode node) {
ASTNode annotationNode = ASTUtils.getNearestAnnotationParent(node);
if (annotationNode != null && annotationNode instanceof NormalAnnotation) {
NormalAnnotation annotation = (NormalAnnotation) annotationNode;
List<?> values = annotation.values();
for (Object value : values) {
if (value instanceof MemberValuePair) {
MemberValuePair valuePair = (MemberValuePair) value;
String valuePairName = valuePair.getName() != null ? valuePair.getName().toString() : null;
if (valuePairName != null && "prefix".equals(valuePairName)
&& valuePair.getValue() != null && valuePair.getValue() instanceof StringLiteral) {
StringLiteral prefixLiteral = (StringLiteral) valuePair.getValue();
String valuePairValue = prefixLiteral.getLiteralValue();
return valuePairValue;
}
}
}
}
return null;
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016, 2023 Pivotal, Inc.
* Copyright (c) 2016, 2024 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
@@ -55,6 +55,11 @@ public class DefaultSpringPropertyIndexProvider implements SpringPropertyIndexPr
return SpringPropertyIndex.EMPTY_INDEX;
}
@Override
public SpringPropertyIndex getIndex(IJavaProject project) {
return indexManager.get(project, progressService);
}
public void setProgressService(ProgressService progressService) {
this.progressService = progressService;
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2015, 2019 Pivotal, Inc.
* Copyright (c) 2015, 2024 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,9 +10,12 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.metadata;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.util.text.IDocument;
public interface SpringPropertyIndexProvider {
SpringPropertyIndex getIndex(IDocument doc);
SpringPropertyIndex getIndex(IJavaProject project);
void onChange(Runnable runnable);
}

View File

@@ -57,6 +57,11 @@ public class PropertyIndexHarness {
}
}
@Override
public SpringPropertyIndex getIndex(IJavaProject project) {
return getIndex((IDocument) null);
}
@Override
public void onChange(Runnable runnable) {

View File

@@ -0,0 +1,286 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom
* 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
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.conditionals.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.gradle.internal.impldep.com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
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.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.ide.vscode.boot.app.BootLanguageServerParams;
import org.springframework.ide.vscode.boot.bootiful.AdHocPropertyHarnessTestConf;
import org.springframework.ide.vscode.boot.bootiful.BootLanguageServerTest;
import org.springframework.ide.vscode.boot.editor.harness.AdHocPropertyHarness;
import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness;
import org.springframework.ide.vscode.boot.index.cache.IndexCache;
import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid;
import org.springframework.ide.vscode.boot.java.links.SourceLinkFactory;
import org.springframework.ide.vscode.boot.java.links.SourceLinks;
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
import org.springframework.ide.vscode.boot.metadata.ValueProviderRegistry;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.languageserver.java.ProjectObserver;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleLanguageServer;
import org.springframework.ide.vscode.commons.languageserver.util.SimpleTextDocumentService;
import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject;
import org.springframework.ide.vscode.commons.util.text.LanguageId;
import org.springframework.ide.vscode.languageserver.testharness.Editor;
import org.springframework.ide.vscode.project.harness.BootLanguageServerHarness;
import org.springframework.ide.vscode.project.harness.ProjectsHarness;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author Martin Lippert
*/
@ExtendWith(SpringExtension.class)
@BootLanguageServerTest
@Import({AdHocPropertyHarnessTestConf.class, ConditionalOnPropertyTest.TestConf.class})
public class ConditionalOnPropertyTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
private Editor editor;
@Autowired private PropertyIndexHarness indexHarness;
@Autowired private AdHocPropertyHarness adHocProperties;
private String tempJavaDocUri;
@Configuration
static class TestConf {
//Somewhat strange test setup, test provides a specific test project.
//The project finder finds this test project,
//But it is not used in the indexProvider/harness.
//this is a bit odd... but we preserved the strangeness how it was.
@Bean MavenJavaProject testProject() throws Exception {
return ProjectsHarness.INSTANCE.mavenProject("test-annotations");
}
@Bean PropertyIndexHarness indexHarness(ValueProviderRegistry valueProviders) {
return new PropertyIndexHarness(valueProviders);
}
@Bean JavaProjectFinder projectFinder(MavenJavaProject testProject) {
return new JavaProjectFinder() {
@Override
public Optional<IJavaProject> find(TextDocumentIdentifier doc) {
return Optional.ofNullable(testProject);
}
@Override
public Collection<? extends IJavaProject> all() {
// TODO Auto-generated method stub
return testProject == null ? Collections.emptyList() : ImmutableList.of(testProject);
}
};
}
@Bean BootLanguageServerHarness harness(SimpleLanguageServer server, BootLanguageServerParams serverParams, PropertyIndexHarness indexHarness, JavaProjectFinder projectFinder) throws Exception {
return new BootLanguageServerHarness(server, serverParams, indexHarness, projectFinder, LanguageId.JAVA, ".java");
}
@Bean BootLanguageServerParams serverParams(SimpleLanguageServer server, JavaProjectFinder projectFinder, ValueProviderRegistry valueProviders, PropertyIndexHarness indexHarness) {
BootLanguageServerParams testDefaults = BootLanguageServerHarness.createTestDefault(server, valueProviders);
return new BootLanguageServerParams(
projectFinder,
ProjectObserver.NULL,
indexHarness.getIndexProvider(),
testDefaults.typeUtilProvider
);
}
@Bean IndexCache symbolCache() {
return new IndexCacheVoid();
}
@Bean SourceLinks sourceLinks(SimpleTextDocumentService documents, CompilationUnitCache cuCache) {
return SourceLinkFactory.NO_SOURCE_LINKS;
}
}
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
File directory = new File(ProjectsHarness.class.getResource("/test-projects/test-annotations/").toURI());
tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
indexHarness.data("spring.boot.prop1", "java.lang.String", null, null);
indexHarness.data("data.prop2", "java.lang.String", null, null);
indexHarness.data("else.prop3", "java.lang.String", null, null);
}
@Test
public void testConditionalOnPropertyCompletionWithoutPrefix() throws Exception {
List<CompletionItem> completions = getCompletions("@ConditionalOnProperty(<*>)");
assertEquals(3, completions.size());
assertEquals("data.prop2", completions.get(0).getLabel());
assertEquals("else.prop3", completions.get(1).getLabel());
assertEquals("spring.boot.prop1", completions.get(2).getLabel());
}
@Test
public void testConditionalOnPropertyCompletionWithoutPrefixAttributeWithNameAttribute() throws Exception {
List<CompletionItem> completions = getCompletions("@ConditionalOnProperty(name=<*>)");
assertEquals(3, completions.size());
assertEquals("data.prop2", completions.get(0).getLabel());
assertEquals("else.prop3", completions.get(1).getLabel());
assertEquals("spring.boot.prop1", completions.get(2).getLabel());
}
@Test
public void testConditionalOnPropertyCompletionWithoutPrefixAttributeWithNameAttributeAndSpaces() throws Exception {
List<CompletionItem> completions = getCompletions("@ConditionalOnProperty(name = <*>)");
assertEquals(3, completions.size());
assertEquals("data.prop2", completions.get(0).getLabel());
assertEquals("else.prop3", completions.get(1).getLabel());
assertEquals("spring.boot.prop1", completions.get(2).getLabel());
}
@Test
public void testConditionalOnPropertyCompletionWithoutPrefixAttributeWithNameAttributeAndSpacesInsideArray() throws Exception {
List<CompletionItem> completions = getCompletions("@ConditionalOnProperty(name = {<*>})");
assertEquals(3, completions.size());
assertEquals("data.prop2", completions.get(0).getLabel());
assertEquals("else.prop3", completions.get(1).getLabel());
assertEquals("spring.boot.prop1", completions.get(2).getLabel());
}
@Test
public void testConditionalOnPropertyCompletionForPrefix() throws Exception {
List<CompletionItem> completions = getCompletions("@ConditionalOnProperty(prefix = <*>)");
assertEquals(4, completions.size());
assertEquals("data", completions.get(0).getLabel());
assertEquals("else", completions.get(1).getLabel());
assertEquals("spring", completions.get(2).getLabel());
assertEquals("spring.boot", completions.get(3).getLabel());
}
@Test
@Disabled
public void testConditionalOnPropertyCompletionWithPrefixAndAttributeWithNameAttribute() throws Exception {
List<CompletionItem> completions = getCompletions("@ConditionalOnProperty(prefix = \"else\", name=<*>)");
assertEquals(1, completions.size());
assertEquals("else.prop3", completions.get(0).getLabel());
assertEquals("prop3", completions.get(0).getFilterText());
assertEquals("prop3", completions.get(0).getTextEdit().getRight().getNewText());
}
@Test
public void testConditionalOnPropertyCompletionWithPrefixAndAttributeWithNameAttributeAndQuotes() throws Exception {
List<CompletionItem> completions = getCompletions("@ConditionalOnProperty(prefix = \"else\", name=\"<*>\")");
assertEquals(1, completions.size());
assertEquals("else.prop3", completions.get(0).getLabel());
assertEquals("prop3", completions.get(0).getFilterText());
assertEquals("prop3", completions.get(0).getTextEdit().getLeft().getNewText());
}
private void assertCompletions(String completionLine, int noOfExpectedCompletions, int selectedProposal, String expectedCompletedLine) throws Exception {
String editorContent = """
package org.test;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
""" +
completionLine + "\n" +
"""
public class TestConditionalOnBeanCompletion {
@Bean
public void method() {
}
}
""";
Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri);
List<CompletionItem> completions = editor.getCompletions();
assertEquals(noOfExpectedCompletions, completions.size());
if (noOfExpectedCompletions > 0) {
editor.apply(completions.get(selectedProposal));
assertEquals("""
package org.test;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
""" +
expectedCompletedLine + "\n" +
"""
public class TestConditionalOnBeanCompletion {
@Bean
public void method() {
}
}
""", editor.getText());
}
}
private List<CompletionItem> getCompletions(String completionLine) throws Exception {
String editorContent = """
package org.test;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
""" +
completionLine + "\n" +
"""
public class TestConditionalOnBeanCompletion {
@Bean
public void method() {
}
}
""";
Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri);
return editor.getCompletions();
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2015, 2016 Pivotal, Inc.
* Copyright (c) 2015, 2024 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
@@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import org.junit.jupiter.api.Test;
import org.springframework.ide.vscode.boot.editor.harness.PropertyIndexHarness;
import org.springframework.ide.vscode.commons.util.text.IDocument;
/**
* Index Navigation tests.
@@ -125,7 +126,7 @@ public class IndexNavigatorTest {
* Reset navigation state to point at the root of the index.
*/
public void start(PropertyIndexHarness harness) {
navigator = IndexNavigator.with(harness.getIndexProvider().getIndex(null).getProperties());
navigator = IndexNavigator.with(harness.getIndexProvider().getIndex((IDocument)null).getProperties());
}
/**

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2016, 2023 Pivotal, Inc.
* Copyright (c) 2016, 2024 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
@@ -47,6 +47,7 @@ import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject;
import org.springframework.ide.vscode.commons.util.RunnableWithException;
import org.springframework.ide.vscode.commons.util.StringUtil;
import org.springframework.ide.vscode.commons.util.text.IDocument;
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;
@@ -4792,7 +4793,7 @@ public class ApplicationYamlEditorTest extends AbstractPropsEditorTest {
useProject(createPredefinedMavenProject("boot-1.3.3-app-with-resource-prop"));
//Check the metadata reflects the 'handle-as':
PropertyInfo metadata = getIndexProvider().getIndex(null).getProperties().get("my.welcome.path");
PropertyInfo metadata = getIndexProvider().getIndex((IDocument)null).getProperties().get("my.welcome.path");
assertEquals("org.springframework.core.io.Resource", metadata.getType());
//Check the content assist based on it works too: