Skip to content

Commit d8e067a

Browse files
committed
GH-1254: add auto-completion for bean names inside of dependson annotation
1 parent 7ee4e07 commit d8e067a

File tree

6 files changed

+561
-17
lines changed

6 files changed

+561
-17
lines changed

headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,17 +151,24 @@ public Editor(LanguageServerHarness harness, String contents, LanguageId languag
151151
this.harness = harness;
152152
this.languageId = LanguageId.of(languageId.getId()); // So we can catch bugs that use == for langauge id comparison.
153153
EditorState state = new EditorState(contents);
154-
this.doc = harness.openDocument(harness.createWorkingCopy(state.documentContents, this.languageId, extension));
154+
155+
String tempUri = harness.createTempUri(extension);
156+
this.doc = harness.openDocument(harness.createDocFromContentWithResource(state.documentContents, tempUri, this.languageId));
157+
155158
this.selectionStart = state.selectionStart;
156159
this.selectionEnd = state.selectionEnd;
157160
this.ignoredTypes = new HashSet<>();
158161
this.highlightsFuture = harness.getHighlightsFuture(doc);
159162
}
163+
160164
public Editor(LanguageServerHarness harness, TextDocumentInfo doc, String contents, LanguageId languageId) throws Exception {
161165
this.harness = harness;
162166
this.languageId = LanguageId.of(languageId.getId()); // So we can catch bugs that use == for langauge id comparison.
163167
EditorState state = new EditorState(contents);
164-
this.doc = harness.openDocument(doc);
168+
169+
String tempUri = doc.getUri();
170+
this.doc = harness.openDocument(harness.createDocFromContentWithResource(state.documentContents, tempUri, languageId));
171+
165172
this.selectionStart = state.selectionStart;
166173
this.selectionEnd = state.selectionEnd;
167174
this.ignoredTypes = new HashSet<>();

headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/LanguageServerHarness.java

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -686,13 +686,13 @@ public synchronized Editor newEditor(LanguageId languageId, String contents) thr
686686

687687
public synchronized Editor newEditor(LanguageId languageId, String contents, String resourceUri) throws Exception {
688688
ensureInitialized();
689-
TextDocumentInfo doc = docFromResource(contents, resourceUri, languageId);
689+
TextDocumentInfo doc = createDocFromContentWithResource(contents, resourceUri, languageId);
690690
Editor editor = new Editor(this, doc, contents, languageId);
691691
activeEditors.add(editor);
692692
return editor;
693693
}
694694

695-
public synchronized TextDocumentInfo docFromResource(String contents, String resourceUri, LanguageId languageId) throws Exception {
695+
public synchronized TextDocumentInfo createDocFromContentWithResource(String contents, String resourceUri, LanguageId languageId) throws Exception {
696696
TextDocumentItem doc = new TextDocumentItem();
697697
doc.setLanguageId(languageId.getId());
698698
doc.setText(contents);
@@ -703,17 +703,6 @@ public synchronized TextDocumentInfo docFromResource(String contents, String res
703703
return docinfo;
704704
}
705705

706-
public synchronized TextDocumentInfo createWorkingCopy(String contents, LanguageId languageId, String extension) throws Exception {
707-
TextDocumentItem doc = new TextDocumentItem();
708-
doc.setLanguageId(languageId.getId());
709-
doc.setText(contents);
710-
doc.setUri(createTempUri(extension));
711-
doc.setVersion(getFirstVersion());
712-
TextDocumentInfo docinfo = new TextDocumentInfo(doc);
713-
documents.put(docinfo.getUri(), docinfo);
714-
return docinfo;
715-
}
716-
717706
protected int getFirstVersion() {
718707
return 1;
719708
}

headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/app/BootJavaCompletionEngineConfigurer.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*******************************************************************************
2-
* Copyright (c) 2020, 2023 Pivotal, Inc.
2+
* Copyright (c) 2020, 2024 Pivotal, Inc.
33
* All rights reserved. This program and the accompanying materials
44
* are made available under the terms of the Eclipse Public License v1.0
55
* which accompanies this distribution, and is available at
@@ -21,8 +21,10 @@
2121
import org.springframework.beans.factory.annotation.Qualifier;
2222
import org.springframework.context.annotation.Bean;
2323
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
2425
import org.springframework.ide.vscode.boot.java.Annotations;
2526
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
27+
import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor;
2628
import org.springframework.ide.vscode.boot.java.data.DataRepositoryCompletionProcessor;
2729
import org.springframework.ide.vscode.boot.java.handlers.BootJavaCompletionEngine;
2830
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
@@ -100,7 +102,8 @@ BootJavaCompletionEngine javaCompletionEngine(
100102
BootLanguageServerParams params,
101103
@Qualifier("adHocProperties") ProjectBasedPropertyIndexProvider adHocProperties,
102104
JavaSnippetManager snippetManager,
103-
CompilationUnitCache cuCache) {
105+
CompilationUnitCache cuCache,
106+
SpringMetamodelIndex springIndex) {
104107

105108
SpringPropertyIndexProvider indexProvider = params.indexProvider;
106109
JavaProjectFinder javaProjectFinder = params.projectFinder;
@@ -109,6 +112,7 @@ BootJavaCompletionEngine javaCompletionEngine(
109112

110113
providers.put(Annotations.SCOPE, new ScopeCompletionProcessor());
111114
providers.put(Annotations.VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocProperties));
115+
providers.put(Annotations.DEPENDS_ON, new DependsOnCompletionProcessor(javaProjectFinder, springIndex));
112116
providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor());
113117

114118
return new BootJavaCompletionEngine(cuCache, providers, snippetManager);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2024 Broadcom
3+
* All rights reserved. This program and the accompanying materials
4+
* are made available under the terms of the Eclipse Public License v1.0
5+
* which accompanies this distribution, and is available at
6+
* https://www.eclipse.org/legal/epl-v10.html
7+
*
8+
* Contributors:
9+
* Broadcom - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.beans;
12+
13+
import java.util.Arrays;
14+
import java.util.Collection;
15+
import java.util.HashSet;
16+
import java.util.List;
17+
import java.util.Optional;
18+
import java.util.Set;
19+
import java.util.stream.Collectors;
20+
21+
import org.eclipse.jdt.core.dom.ASTNode;
22+
import org.eclipse.jdt.core.dom.Annotation;
23+
import org.eclipse.jdt.core.dom.ArrayInitializer;
24+
import org.eclipse.jdt.core.dom.ITypeBinding;
25+
import org.eclipse.jdt.core.dom.MemberValuePair;
26+
import org.eclipse.jdt.core.dom.SimpleName;
27+
import org.eclipse.jdt.core.dom.StringLiteral;
28+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
29+
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
30+
import org.springframework.ide.vscode.commons.java.IJavaProject;
31+
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
32+
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
33+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
34+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
35+
import org.springframework.ide.vscode.commons.util.BadLocationException;
36+
import org.springframework.ide.vscode.commons.util.text.IDocument;
37+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
38+
39+
/**
40+
* @author Martin Lippert
41+
*/
42+
public class DependsOnCompletionProcessor implements CompletionProvider {
43+
44+
private final JavaProjectFinder projectFinder;
45+
private final SpringMetamodelIndex springIndex;
46+
47+
public DependsOnCompletionProcessor(JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) {
48+
this.projectFinder = projectFinder;
49+
this.springIndex = springIndex;
50+
}
51+
52+
@Override
53+
public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type, int offset, TextDocument doc, Collection<ICompletionProposal> completions) {
54+
55+
Optional<IJavaProject> optionalProject = projectFinder.find(doc.getId());
56+
if (!optionalProject.isPresent()) {
57+
return;
58+
}
59+
60+
IJavaProject project = optionalProject.get();
61+
62+
try {
63+
64+
// case: @DependsOn(<*>)
65+
if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) {
66+
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
67+
68+
for (Bean bean : beans) {
69+
70+
DocumentEdits edits = new DocumentEdits(doc, false);
71+
edits.replace(offset, offset, "\"" + bean.getName() + "\"");
72+
73+
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
74+
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
75+
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
76+
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
77+
78+
completions.add(proposal);
79+
}
80+
}
81+
// case: @DependsOn(prefix<*>)
82+
else if (node instanceof SimpleName && node.getParent() instanceof Annotation) {
83+
computeProposalsForSimpleName(project, node, completions, offset, doc);
84+
}
85+
// case: @DependsOn(value=<*>)
86+
else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair
87+
&& "value".equals(((MemberValuePair)node.getParent()).getName().toString())) {
88+
computeProposalsForSimpleName(project, node, completions, offset, doc);
89+
}
90+
// case: @DependsOn("prefix<*>")
91+
else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) {
92+
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
93+
computeProposalsForStringLiteral(project, node, completions, offset, doc);
94+
}
95+
}
96+
else if (node instanceof StringLiteral && node.getParent() instanceof ArrayInitializer) {
97+
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
98+
computeProposalsForInsideArrayInitializer(project, node, completions, offset, doc);
99+
}
100+
}
101+
// case: @DependsOn(value="prefix<*>")
102+
else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair
103+
&& "value".equals(((MemberValuePair)node.getParent()).getName().toString())) {
104+
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
105+
computeProposalsForStringLiteral(project, node, completions, offset, doc);
106+
}
107+
}
108+
// case: @DependsOn({<*>})
109+
else if (node instanceof ArrayInitializer && node.getParent() instanceof Annotation) {
110+
computeProposalsForArrayInitializr(project, (ArrayInitializer) node, completions, offset, doc);
111+
}
112+
}
113+
catch (Exception e) {
114+
e.printStackTrace();
115+
}
116+
}
117+
118+
private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, IDocument doc) {
119+
String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition());
120+
121+
int startOffset = node.getStartPosition();
122+
int endOffset = node.getStartPosition() + node.getLength();
123+
124+
String proposalPrefix = "\"";
125+
String proposalPostfix = "\"";
126+
127+
Set<String> mentionedBeans = alreadyMentionedBeans(node);
128+
129+
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
130+
List<Bean> matchingBeans = Arrays.stream(beans)
131+
.filter(bean -> bean.getName().toLowerCase().startsWith(prefix.toLowerCase()))
132+
.filter(bean -> !mentionedBeans.contains(bean.getName()))
133+
.collect(Collectors.toList());
134+
135+
for (Bean bean : matchingBeans) {
136+
137+
DocumentEdits edits = new DocumentEdits(doc, false);
138+
edits.replace(startOffset, endOffset, proposalPrefix + bean.getName() + proposalPostfix);
139+
140+
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
141+
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
142+
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
143+
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
144+
145+
completions.add(proposal);
146+
}
147+
}
148+
149+
private void computeProposalsForStringLiteral(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, IDocument doc) throws BadLocationException {
150+
int length = offset - (node.getStartPosition() + 1);
151+
152+
String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, length), length);
153+
int startOffset = offset - prefix.length();
154+
int endOffset = offset;
155+
156+
Set<String> mentionedBeans = alreadyMentionedBeans(node);
157+
158+
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
159+
160+
final String filterPrefix = prefix;
161+
List<Bean> matchingBeans = Arrays.stream(beans)
162+
.filter(bean -> bean.getName().toLowerCase().startsWith(filterPrefix.toLowerCase()))
163+
.filter(bean -> !mentionedBeans.contains(bean.getName()))
164+
.collect(Collectors.toList());
165+
166+
for (Bean bean : matchingBeans) {
167+
168+
DocumentEdits edits = new DocumentEdits(doc, false);
169+
edits.replace(startOffset, endOffset, bean.getName());
170+
171+
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
172+
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
173+
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
174+
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
175+
176+
completions.add(proposal);
177+
}
178+
}
179+
180+
private void computeProposalsForArrayInitializr(IJavaProject project, ArrayInitializer node, Collection<ICompletionProposal> completions, int offset, IDocument doc) {
181+
Set<String> mentionedBeans = alreadyMentionedBeans(node);
182+
183+
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
184+
List<Bean> filteredBeans = Arrays.stream(beans)
185+
.filter(bean -> !mentionedBeans.contains(bean.getName()))
186+
.collect(Collectors.toList());
187+
188+
for (Bean bean : filteredBeans) {
189+
190+
DocumentEdits edits = new DocumentEdits(doc, false);
191+
edits.replace(offset, offset, "\"" + bean.getName() + "\"");
192+
193+
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
194+
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
195+
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
196+
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
197+
198+
completions.add(proposal);
199+
}
200+
}
201+
202+
private void computeProposalsForInsideArrayInitializer(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) throws BadLocationException {
203+
int length = offset - (node.getStartPosition() + 1);
204+
if (length >= 0) {
205+
computeProposalsForStringLiteral(project, node, completions, offset, doc);
206+
}
207+
else {
208+
Set<String> mentionedBeans = alreadyMentionedBeans(node);
209+
210+
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
211+
List<Bean> filteredBeans = Arrays.stream(beans)
212+
.filter(bean -> !mentionedBeans.contains(bean.getName()))
213+
.collect(Collectors.toList());
214+
215+
for (Bean bean : filteredBeans) {
216+
217+
DocumentEdits edits = new DocumentEdits(doc, false);
218+
edits.replace(offset, offset, "\"" + bean.getName() + "\",");
219+
220+
// PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet
221+
// and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string
222+
// if it cannot resolve it. If sending this as plain text, then insertion happens correctly
223+
DependsOnCompletionProposal proposal = new DependsOnCompletionProposal(edits, bean.getName(), bean.getName(), null);
224+
225+
completions.add(proposal);
226+
}
227+
}
228+
}
229+
230+
private String identifyPropertyPrefix(String nodeContent, int offset) {
231+
String result = nodeContent.substring(0, offset);
232+
233+
int i = offset - 1;
234+
while (i >= 0) {
235+
char c = nodeContent.charAt(i);
236+
if (c == '}' || c == '{' || c == '$' || c == '#') {
237+
result = result.substring(i + 1, offset);
238+
break;
239+
}
240+
i--;
241+
}
242+
243+
return result;
244+
}
245+
246+
private Set<String> alreadyMentionedBeans(ASTNode node) {
247+
Set<String> result = new HashSet<>();
248+
249+
ArrayInitializer arrayNode = null;
250+
while (node != null && arrayNode == null && !(node instanceof Annotation)) {
251+
if (node instanceof ArrayInitializer) {
252+
arrayNode = (ArrayInitializer) node;
253+
}
254+
else {
255+
node = node.getParent();
256+
}
257+
}
258+
259+
if (arrayNode != null) {
260+
List<?> expressions = arrayNode.expressions();
261+
for (Object expression : expressions) {
262+
if (expression instanceof StringLiteral) {
263+
StringLiteral stringExr = (StringLiteral) expression;
264+
String value = stringExr.getLiteralValue();
265+
result.add(value);
266+
}
267+
}
268+
}
269+
270+
return result;
271+
}
272+
273+
274+
275+
}

0 commit comments

Comments
 (0)