Skip to content

Commit 992a7c7

Browse files
Add file completion from classpath for @ContextConfiguration (#1318)
* Add file completion from classpath for @ContextConfiguration * Update copyright to Broadcom --------- Co-authored-by: ksankaranara <[email protected]>
1 parent ece6380 commit 992a7c7

File tree

16 files changed

+911
-0
lines changed

16 files changed

+911
-0
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
4040
import org.springframework.ide.vscode.boot.java.utils.CompilationUnitCache;
4141
import org.springframework.ide.vscode.boot.java.value.ValueCompletionProcessor;
42+
import org.springframework.ide.vscode.boot.java.contextconfiguration.ContextConfigurationProcessor;
4243
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
4344
import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider;
4445
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
@@ -115,6 +116,7 @@ BootJavaCompletionEngine javaCompletionEngine(
115116
Map<String, CompletionProvider> providers = new HashMap<>();
116117

117118
providers.put(Annotations.VALUE, new ValueCompletionProcessor(javaProjectFinder, indexProvider, adHocProperties));
119+
providers.put(Annotations.CONTEXT_CONFIGURATION, new ContextConfigurationProcessor(javaProjectFinder));
118120
providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor());
119121

120122
providers.put(Annotations.SCOPE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new ScopeCompletionProcessor())));

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

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public class Annotations {
7373
public static final String VALUE = "org.springframework.beans.factory.annotation.Value";
7474
public static final String SCOPE = "org.springframework.context.annotation.Scope";
7575
public static final String DEPENDS_ON = "org.springframework.context.annotation.DependsOn";
76+
public static final String CONTEXT_CONFIGURATION = "org.springframework.test.context.ContextConfiguration";
7677

7778
public static final String RESOURCE_JAVAX = "javax.annotation.Resource";
7879
public static final String RESOURCE_JAKARTA = "jakarta.annotation.Resource";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2017, 2024 Broadcom, Inc.
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, Inc. - initial API and implementation
10+
*******************************************************************************/
11+
package org.springframework.ide.vscode.boot.java.contextconfiguration;
12+
13+
import static org.springframework.ide.vscode.commons.util.StringUtil.camelCaseToHyphens;
14+
15+
import java.nio.file.Paths;
16+
import java.util.*;
17+
import java.util.stream.Collectors;
18+
19+
import org.eclipse.jdt.core.dom.ASTNode;
20+
import org.eclipse.jdt.core.dom.Annotation;
21+
import org.eclipse.jdt.core.dom.ITypeBinding;
22+
import org.eclipse.jdt.core.dom.MemberValuePair;
23+
import org.eclipse.jdt.core.dom.QualifiedName;
24+
import org.eclipse.jdt.core.dom.SimpleName;
25+
import org.eclipse.jdt.core.dom.StringLiteral;
26+
import org.eclipse.lsp4j.TextDocumentIdentifier;
27+
import org.openrewrite.yaml.internal.grammar.JsonPathParser;
28+
import org.slf4j.Logger;
29+
import org.slf4j.LoggerFactory;
30+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProposal;
31+
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
32+
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
33+
import org.springframework.ide.vscode.boot.metadata.PropertyInfo;
34+
import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider;
35+
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
36+
import org.springframework.ide.vscode.commons.java.IJavaProject;
37+
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
38+
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
39+
import org.springframework.ide.vscode.commons.languageserver.java.JavaProjectFinder;
40+
import org.springframework.ide.vscode.commons.util.BadLocationException;
41+
import org.springframework.ide.vscode.commons.util.FuzzyMap;
42+
import org.springframework.ide.vscode.commons.util.FuzzyMap.Match;
43+
import org.springframework.ide.vscode.commons.util.text.IDocument;
44+
import org.springframework.ide.vscode.commons.util.text.TextDocument;
45+
46+
/**
47+
* @author Karthik Sankaranarayanan
48+
*/
49+
public class ContextConfigurationProcessor implements CompletionProvider {
50+
51+
private static final Logger log = LoggerFactory.getLogger(ContextConfigurationProcessor.class);
52+
53+
private final JavaProjectFinder projectFinder;
54+
55+
public ContextConfigurationProcessor(JavaProjectFinder projectFinder) {
56+
this.projectFinder = projectFinder;
57+
}
58+
59+
@Override
60+
public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type,
61+
int offset, TextDocument doc, Collection<ICompletionProposal> completions) {
62+
63+
try {
64+
Optional<IJavaProject> optionalProject = this.projectFinder.find(doc.getId());
65+
if (optionalProject.isEmpty()) {
66+
return;
67+
}
68+
69+
IJavaProject project = optionalProject.get();
70+
71+
// case: @ContextConfiguration(<*>)
72+
if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) {
73+
addClasspathResourceProposals(project, doc, offset, offset, "", true, completions);
74+
}
75+
// case: @ContextConfiguration(prefix<*>)
76+
else if (node instanceof SimpleName && node.getParent() instanceof Annotation) {
77+
computeProposalsForSimpleName(project, node, completions, offset, doc);
78+
}
79+
// case: @ContextConfiguration(file.ext<*>) - the "." causes a QualifierNode to be generated
80+
else if (node instanceof SimpleName && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof Annotation) {
81+
computeProposalsForSimpleName(project, node.getParent(), completions, offset, doc);
82+
}
83+
// case: @ContextConfiguration(locations=<*>) || @ContextConfiguration(value=<*>)
84+
else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair
85+
&& ("locations".equals(((MemberValuePair)node.getParent()).getName().toString()) || "value".equals(((MemberValuePair)node.getParent()).getName().toString()))) {
86+
computeProposalsForSimpleName(project, node, completions, offset, doc);
87+
}
88+
// case: @ContextConfiguration(locations=<*>) || @ContextConfiguration(value=<*>)
89+
else if (node instanceof SimpleName && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof MemberValuePair
90+
&& ("locations".equals(((MemberValuePair)node.getParent().getParent()).getName().toString()) || "value".equals(((MemberValuePair)node.getParent().getParent()).getName().toString()))) {
91+
computeProposalsForSimpleName(project, node.getParent(), completions, offset, doc);
92+
}
93+
// case: @ContextConfiguration("prefix<*>")
94+
else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) {
95+
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
96+
computeProposalsForStringLiteral(project, (StringLiteral) node, completions, offset, doc);
97+
}
98+
}
99+
// case:@ContextConfiguration(locations="prefix<*>") || @ContextConfiguration(value="prefix<*>")
100+
else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair
101+
&& ("locations".equals(((MemberValuePair)node.getParent()).getName().toString()) || "value".equals(((MemberValuePair)node.getParent()).getName().toString()))) {
102+
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
103+
computeProposalsForStringLiteral(project, (StringLiteral) node, completions, offset, doc);
104+
}
105+
}
106+
107+
}
108+
catch (Exception e) {
109+
log.error("problem while looking for ContextConfiguration annotation proposals", e);
110+
}
111+
}
112+
113+
private void addClasspathResourceProposals(IJavaProject project, TextDocument doc, int startOffset, int endOffset, String prefix, boolean includeQuotes, Collection<ICompletionProposal> completions) {
114+
String[] resources = findResources(project, prefix);
115+
List<String> result = Arrays.asList(resources);
116+
List<String> filteredResult = result.stream().filter(f -> f.endsWith(".xml")).toList();
117+
double score = resources.length + 1000;
118+
for (String resource : filteredResult) {
119+
120+
DocumentEdits edits = new DocumentEdits(doc, false);
121+
122+
if (includeQuotes) {
123+
edits.replace(startOffset, endOffset, "\"" + "/" + resource + "\"");
124+
}
125+
else {
126+
edits.replace(startOffset, endOffset, "/" + resource);
127+
}
128+
129+
String label = "/" + resource;
130+
131+
ICompletionProposal proposal = new AnnotationAttributeCompletionProposal(edits, label, label, null, score--);
132+
completions.add(proposal);
133+
}
134+
135+
}
136+
137+
private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) {
138+
int startOffset = node.getStartPosition();
139+
int endOffset = node.getStartPosition() + node.getLength();
140+
141+
String unfilteredPrefix = node.toString().substring(0, offset - node.getStartPosition());
142+
addClasspathResourceProposals(project, doc, startOffset, endOffset, unfilteredPrefix, true, completions);
143+
}
144+
145+
private void computeProposalsForStringLiteral(IJavaProject project, StringLiteral node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) throws BadLocationException {
146+
String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, offset - (node.getStartPosition() + 1)), offset - (node.getStartPosition() + 1));
147+
148+
int startOffset = offset - prefix.length();
149+
int endOffset = offset;
150+
151+
String unfilteredPrefix = node.getLiteralValue().substring(0, offset - (node.getStartPosition() + 1));
152+
addClasspathResourceProposals(project, doc, startOffset, endOffset, unfilteredPrefix, false, completions);
153+
}
154+
155+
public String identifyPropertyPrefix(String nodeContent, int offset) {
156+
String result = nodeContent.substring(0, offset);
157+
158+
int i = offset - 1;
159+
while (i >= 0) {
160+
char c = nodeContent.charAt(i);
161+
if (c == '}' || c == '{' || c == '$' || c == '#') {
162+
result = result.substring(i + 1, offset);
163+
break;
164+
}
165+
i--;
166+
}
167+
168+
return result;
169+
}
170+
171+
private String[] findResources(IJavaProject project, String prefix) {
172+
String[] resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream()
173+
.distinct()
174+
.sorted(new Comparator<String>() {
175+
@Override
176+
public int compare(String o1, String o2) {
177+
return Paths.get(o1).compareTo(Paths.get(o2));
178+
}
179+
})
180+
.map(r -> r.replaceAll("\\\\", "/"))
181+
.filter(r -> ("classpath:" + r).contains(prefix))
182+
.toArray(String[]::new);
183+
184+
return resources;
185+
}
186+
187+
}

0 commit comments

Comments
 (0)