From d8e067ad9d36453eefbf0239f0c56b675c58fce4 Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 30 May 2024 17:11:58 +0200 Subject: [PATCH] GH-1254: add auto-completion for bean names inside of dependson annotation --- .../languageserver/testharness/Editor.java | 11 +- .../testharness/LanguageServerHarness.java | 15 +- .../BootJavaCompletionEngineConfigurer.java | 8 +- .../beans/DependsOnCompletionProcessor.java | 275 ++++++++++++++++++ .../beans/DependsOnCompletionProposal.java | 65 +++++ .../test/DependsOnCompletionProviderTest.java | 204 +++++++++++++ 6 files changed, 561 insertions(+), 17 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProposal.java create mode 100644 headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index c5eaeba69..cd428389b 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -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<>(); diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java index c1bd11940..8d0245fd1 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java @@ -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; } 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 cfbafec33..6455f670c 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 @@ -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); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java new file mode 100644 index 000000000..1fcfe6ed7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProcessor.java @@ -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 completions) { + + Optional 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 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 mentionedBeans = alreadyMentionedBeans(node); + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + List 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 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 mentionedBeans = alreadyMentionedBeans(node); + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + + final String filterPrefix = prefix; + List 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 completions, int offset, IDocument doc) { + Set mentionedBeans = alreadyMentionedBeans(node); + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + List 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 completions, int offset, TextDocument doc) throws BadLocationException { + int length = offset - (node.getStartPosition() + 1); + if (length >= 0) { + computeProposalsForStringLiteral(project, node, completions, offset, doc); + } + else { + Set mentionedBeans = alreadyMentionedBeans(node); + + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + List 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 alreadyMentionedBeans(ASTNode node) { + Set 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; + } + + + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProposal.java new file mode 100644 index 000000000..a2e1f4382 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/DependsOnCompletionProposal.java @@ -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; + } + +} 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 new file mode 100644 index 000000000..d155241a8 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/DependsOnCompletionProviderTest.java @@ -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 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 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()); + } + } + + +}