GH-1254: add auto-completion for bean names inside of dependson annotation

This commit is contained in:
Martin Lippert
2024-05-30 17:11:58 +02:00
parent 7ee4e07223
commit d8e067ad9d
6 changed files with 561 additions and 17 deletions

View File

@@ -151,17 +151,24 @@ public class Editor {
this.harness = harness;
this.languageId = LanguageId.of(languageId.getId()); // So we can catch bugs that use == for langauge id comparison.
EditorState state = new EditorState(contents);
this.doc = harness.openDocument(harness.createWorkingCopy(state.documentContents, this.languageId, extension));
String tempUri = harness.createTempUri(extension);
this.doc = harness.openDocument(harness.createDocFromContentWithResource(state.documentContents, tempUri, this.languageId));
this.selectionStart = state.selectionStart;
this.selectionEnd = state.selectionEnd;
this.ignoredTypes = new HashSet<>();
this.highlightsFuture = harness.getHighlightsFuture(doc);
}
public Editor(LanguageServerHarness harness, TextDocumentInfo doc, String contents, LanguageId languageId) throws Exception {
this.harness = harness;
this.languageId = LanguageId.of(languageId.getId()); // So we can catch bugs that use == for langauge id comparison.
EditorState state = new EditorState(contents);
this.doc = harness.openDocument(doc);
String tempUri = doc.getUri();
this.doc = harness.openDocument(harness.createDocFromContentWithResource(state.documentContents, tempUri, languageId));
this.selectionStart = state.selectionStart;
this.selectionEnd = state.selectionEnd;
this.ignoredTypes = new HashSet<>();

View File

@@ -686,13 +686,13 @@ public class LanguageServerHarness {
public synchronized Editor newEditor(LanguageId languageId, String contents, String resourceUri) throws Exception {
ensureInitialized();
TextDocumentInfo doc = docFromResource(contents, resourceUri, languageId);
TextDocumentInfo doc = createDocFromContentWithResource(contents, resourceUri, languageId);
Editor editor = new Editor(this, doc, contents, languageId);
activeEditors.add(editor);
return editor;
}
public synchronized TextDocumentInfo docFromResource(String contents, String resourceUri, LanguageId languageId) throws Exception {
public synchronized TextDocumentInfo createDocFromContentWithResource(String contents, String resourceUri, LanguageId languageId) throws Exception {
TextDocumentItem doc = new TextDocumentItem();
doc.setLanguageId(languageId.getId());
doc.setText(contents);
@@ -703,17 +703,6 @@ public class LanguageServerHarness {
return docinfo;
}
public synchronized TextDocumentInfo createWorkingCopy(String contents, LanguageId languageId, String extension) throws Exception {
TextDocumentItem doc = new TextDocumentItem();
doc.setLanguageId(languageId.getId());
doc.setText(contents);
doc.setUri(createTempUri(extension));
doc.setVersion(getFirstVersion());
TextDocumentInfo docinfo = new TextDocumentInfo(doc);
documents.put(docinfo.getUri(), docinfo);
return docinfo;
}
protected int getFirstVersion() {
return 1;
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2020, 2023 Pivotal, Inc.
* Copyright (c) 2020, 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
@@ -21,8 +21,10 @@ import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor;
import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor;
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
@@ -100,7 +102,8 @@ public class BootJavaCompletionEngineConfigurer {
BootLanguageServerParams params,
@Qualifier("adHocProperties") ProjectBasedPropertyIndexProvider adHocProperties,
JavaSnippetManager snippetManager,
CompilationUnitCache cuCache) {
CompilationUnitCache cuCache,
SpringMetamodelIndex springIndex) {
SpringPropertyIndexProvider indexProvider = params.indexProvider;
JavaProjectFinder javaProjectFinder = params.projectFinder;
@@ -109,6 +112,7 @@ public class BootJavaCompletionEngineConfigurer {
providers.put(Annotations.SCOPE, new ScopeCompletionProcessor());
providers.put(Annotations.VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocProperties));
providers.put(Annotations.DEPENDS_ON, new DependsOnCompletionProcessor(javaProjectFinder, springIndex));
providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor());
return new BootJavaCompletionEngine(cuCache, providers, snippetManager);

View File

@@ -0,0 +1,275 @@
/*******************************************************************************
* 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.beans;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.eclipse.jdt.core.dom.ASTNode;
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.SimpleName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
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.protocol.spring.Bean;
import org.springframework.ide.vscode.commons.util.BadLocationException;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
/**
* @author Martin Lippert
*/
public class DependsOnCompletionProcessor implements CompletionProvider {
private final JavaProjectFinder projectFinder;
private final SpringMetamodelIndex springIndex;
public DependsOnCompletionProcessor(JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) {
this.projectFinder = projectFinder;
this.springIndex = springIndex;
}
@Override
public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc, Collection<ICompletionProposal> completions) {
Optional<IJavaProject> optionalProject = projectFinder.find(doc.getId());
if (!optionalProject.isPresent()) {
return;
}
IJavaProject project = optionalProject.get();
try {
// case: @DependsOn(<*>)
if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) {
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
for (Bean bean : beans) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(offset, offset, "\"" + bean.getName() + "\"");
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
completions.add(proposal);
}
}
// case: @DependsOn(prefix<*>)
else if (node instanceof SimpleName && node.getParent() instanceof Annotation) {
computeProposalsForSimpleName(project, node, completions, offset, doc);
}
// case: @DependsOn(value=<*>)
else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair
&& "value".equals(((MemberValuePair)node.getParent()).getName().toString())) {
computeProposalsForSimpleName(project, node, completions, offset, doc);
}
// case: @DependsOn("prefix<*>")
else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) {
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
computeProposalsForStringLiteral(project, node, completions, offset, doc);
}
}
else if (node instanceof StringLiteral && node.getParent() instanceof ArrayInitializer) {
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
computeProposalsForInsideArrayInitializer(project, node, completions, offset, doc);
}
}
// case: @DependsOn(value="prefix<*>")
else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair
&& "value".equals(((MemberValuePair)node.getParent()).getName().toString())) {
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
computeProposalsForStringLiteral(project, node, completions, offset, doc);
}
}
// case: @DependsOn({<*>})
else if (node instanceof ArrayInitializer && node.getParent() instanceof Annotation) {
computeProposalsForArrayInitializr(project, (ArrayInitializer) node, completions, offset, doc);
}
}
catch (Exception e) {
e.printStackTrace();
}
}
private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, IDocument doc) {
String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition());
int startOffset = node.getStartPosition();
int endOffset = node.getStartPosition() + node.getLength();
String proposalPrefix = "\"";
String proposalPostfix = "\"";
Set<String> mentionedBeans = alreadyMentionedBeans(node);
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
List<Bean> matchingBeans = Arrays.stream(beans)
.filter(bean -> bean.getName().toLowerCase().startsWith(prefix.toLowerCase()))
.filter(bean -> !mentionedBeans.contains(bean.getName()))
.collect(Collectors.toList());
for (Bean bean : matchingBeans) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(startOffset, endOffset, proposalPrefix + bean.getName() + proposalPostfix);
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
completions.add(proposal);
}
}
private void computeProposalsForStringLiteral(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, IDocument doc) throws BadLocationException {
int length = offset - (node.getStartPosition() + 1);
String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, length), length);
int startOffset = offset - prefix.length();
int endOffset = offset;
Set<String> mentionedBeans = alreadyMentionedBeans(node);
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
final String filterPrefix = prefix;
List<Bean> matchingBeans = Arrays.stream(beans)
.filter(bean -> bean.getName().toLowerCase().startsWith(filterPrefix.toLowerCase()))
.filter(bean -> !mentionedBeans.contains(bean.getName()))
.collect(Collectors.toList());
for (Bean bean : matchingBeans) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(startOffset, endOffset, bean.getName());
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
completions.add(proposal);
}
}
private void computeProposalsForArrayInitializr(IJavaProject project, ArrayInitializer node, Collection<ICompletionProposal> completions, int offset, IDocument doc) {
Set<String> mentionedBeans = alreadyMentionedBeans(node);
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
List<Bean> filteredBeans = Arrays.stream(beans)
.filter(bean -> !mentionedBeans.contains(bean.getName()))
.collect(Collectors.toList());
for (Bean bean : filteredBeans) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(offset, offset, "\"" + bean.getName() + "\"");
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
completions.add(proposal);
}
}
private void computeProposalsForInsideArrayInitializer(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) throws BadLocationException {
int length = offset - (node.getStartPosition() + 1);
if (length >= 0) {
computeProposalsForStringLiteral(project, node, completions, offset, doc);
}
else {
Set<String> mentionedBeans = alreadyMentionedBeans(node);
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
List<Bean> filteredBeans = Arrays.stream(beans)
.filter(bean -> !mentionedBeans.contains(bean.getName()))
.collect(Collectors.toList());
for (Bean bean : filteredBeans) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(offset, offset, "\"" + bean.getName() + "\",");
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
completions.add(proposal);
}
}
}
private 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 Set<String> alreadyMentionedBeans(ASTNode node) {
Set<String> result = new HashSet<>();
ArrayInitializer arrayNode = null;
while (node != null && arrayNode == null && !(node instanceof Annotation)) {
if (node instanceof ArrayInitializer) {
arrayNode = (ArrayInitializer) node;
}
else {
node = node.getParent();
}
}
if (arrayNode != null) {
List<?> expressions = arrayNode.expressions();
for (Object expression : expressions) {
if (expression instanceof StringLiteral) {
StringLiteral stringExr = (StringLiteral) expression;
String value = stringExr.getLiteralValue();
result.add(value);
}
}
}
return result;
}
}

View File

@@ -0,0 +1,65 @@
/*******************************************************************************
* 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.beans;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.util.Renderable;
/**
* @author Martin Lippert
*/
public class DependsOnCompletionProposal implements ICompletionProposal {
private static final String EMPTY_DETAIL = "";
private DocumentEdits edits;
private String label;
private String detail;
private Renderable documentation;
public DependsOnCompletionProposal(DocumentEdits edits, String label, String detail, Renderable documentation) {
this.edits = edits;
this.label = label;
// PT 161489998 - Detail for proposal must not be null. For some clients like Eclipse,
// a null detail results in an NPE at JDT level when inserting the proposal in the editor, and results
// in odd behaviour like insertion of an extra new line.
this.detail = detail == null ? EMPTY_DETAIL : detail;
this.documentation = documentation;
}
@Override
public String getLabel() {
return this.label;
}
@Override
public CompletionItemKind getKind() {
return CompletionItemKind.Value;
}
@Override
public DocumentEdits getTextEdit() {
return this.edits;
}
@Override
public String getDetail() {
return this.detail;
}
@Override
public Renderable getDocumentation() {
return this.documentation;
}
}

View File

@@ -0,0 +1,204 @@
/*******************************************************************************
* 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.beans.test;
import static org.junit.Assert.assertEquals;
import java.io.File;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.lsp4j.CompletionItem;
import org.eclipse.lsp4j.Location;
import org.eclipse.lsp4j.Position;
import org.eclipse.lsp4j.Range;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.junit.jupiter.api.AfterEach;
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.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
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(SymbolProviderTestConf.class)
public class DependsOnCompletionProviderTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private JavaProjectFinder projectFinder;
@Autowired private SpringMetamodelIndex springIndex;
@Autowired private SpringSymbolIndex indexer;
private File directory;
private IJavaProject project;
private Bean[] indexedBeans;
private String tempJavaDocUri;
private Bean bean1;
private Bean bean2;
@BeforeEach
public void setup() throws Exception {
harness.intialize(null);
directory = new File(ProjectsHarness.class.getResource("/test-projects/test-spring-indexing/").toURI());
String projectDir = directory.toURI().toString();
project = projectFinder.find(new TextDocumentIdentifier(projectDir)).get();
CompletableFuture<Void> initProject = indexer.waitOperation();
initProject.get(5, TimeUnit.SECONDS);
indexedBeans = springIndex.getBeansOfProject(project.getElementName());
tempJavaDocUri = directory.toPath().resolve("src/main/java/org/test/TempClass.java").toUri().toString();
bean1 = new Bean("bean1", "type1", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null);
bean2 = new Bean("bean2", "type2", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null);
springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2});
}
@AfterEach
public void restoreIndexState() {
this.springIndex.updateBeans(project.getElementName(), indexedBeans);
}
@Test
public void testDependsOnCompletionWithoutQuotesWithoutPrefix() throws Exception {
assertCompletions("@DependsOn(<*>)", 2, "@DependsOn(\"bean1\"<*>)");
}
@Test
public void testDependsOnCompletionWithoutQuotesWithPrefix() throws Exception {
assertCompletions("@DependsOn(be<*>)", 2, "@DependsOn(\"bean1\"<*>)");
}
@Test
public void testDependsOnCompletionWithoutQuotesWithAttributeName() throws Exception {
assertCompletions("@DependsOn(value=<*>)", 2, "@DependsOn(value=\"bean1\"<*>)");
}
@Test
public void testDependsOnCompletionInsideOfQuotesWithoutPrefix() throws Exception {
assertCompletions("@DependsOn(\"<*>\")", 2, "@DependsOn(\"bean1<*>\")");
}
@Test
public void testDependsOnCompletionWithoutQuotesWithoutPrefixInsideArray() throws Exception {
assertCompletions("@DependsOn({<*>})", 2, "@DependsOn({\"bean1\"<*>})");
}
@Test
public void testDependsOnCompletionInsideOfQuotesWithoutPrefixInsideArray() throws Exception {
assertCompletions("@DependsOn({\"<*>\"})", 2, "@DependsOn({\"bean1<*>\"})");
}
@Test
public void testDependsOnCompletionInsideOfQuotesWithPrefix() throws Exception {
assertCompletions("@DependsOn(\"be<*>\")", 2, "@DependsOn(\"bean1<*>\")");
}
@Test
public void testDependsOnCompletionInsideOfQuotesAndArrayWithPrefix() throws Exception {
assertCompletions("@DependsOn({\"be<*>\"})", 2, "@DependsOn({\"bean1<*>\"})");
}
@Test
public void testDependsOnCompletionInsideOfQuotesWithPrefixButWithoutMatches() throws Exception {
assertCompletions("@DependsOn(\"XXX<*>\")", 0, null);
}
@Test
public void testDependsOnCompletionOutsideOfAnnotation1() throws Exception {
assertCompletions("@DependsOn(\"XXX\")<*>", 0, null);
}
@Test
public void testDependsOnCompletionOutsideOfAnnotation2() throws Exception {
assertCompletions("@DependsOn<*>(\"XXX\")", 0, null);
}
@Test
public void testDependsOnCompletionInsideOfQuotesWithPrefixAndReplacedPostfix() throws Exception {
assertCompletions("@DependsOn(\"be<*>xxx\")", 2, "@DependsOn(\"bean1<*>xxx\")");
}
@Test
public void testDependsOnCompletionInsideOfArrayBehindExistingElement() throws Exception {
assertCompletions("@DependsOn({\"bean1\",<*>})", 1, "@DependsOn({\"bean1\",\"bean2\"<*>})");
}
@Test
public void testDependsOnCompletionInsideOfArrayInFrontOfExistingElement() throws Exception {
assertCompletions("@DependsOn({<*>\"bean1\"})", 1, "@DependsOn({\"bean2\",<*>\"bean1\"})");
}
@Test
public void testDependsOnCompletionInsideOfArrayBetweenExistingElements() throws Exception {
Bean bean3 = new Bean("bean3", "type3", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null);
springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3});
assertCompletions("@DependsOn({\"bean1\",<*>\"bean2\"})", 1, "@DependsOn({\"bean1\",\"bean3\",<*>\"bean2\"})");
}
private void assertCompletions(String completionLine, int noOfExcpectedCompletions, String expectedCompletedLine) throws Exception {
String editorContent = """
package org.test;
import org.springframework.context.annotation.DependsOn;
@Component
""" +
completionLine + "\n" +
"""
public class TestDependsOnClass {
}
""";
Editor editor = harness.newEditor(LanguageId.JAVA, editorContent, tempJavaDocUri);
List<CompletionItem> completions = editor.getCompletions();
assertEquals(noOfExcpectedCompletions, completions.size());
if (noOfExcpectedCompletions > 0) {
editor.apply(completions.get(0));
assertEquals("""
package org.test;
import org.springframework.context.annotation.DependsOn;
@Component
""" + expectedCompletedLine + "\n" +
"""
public class TestDependsOnClass {
}
""", editor.getText());
}
}
}