Skip to content

Commit 3f183e6

Browse files
committed
GH-1313: added support for content-assist, find references, and go to definition for Named annotation usage in Spring apps
1 parent 370ac52 commit 3f183e6

File tree

11 files changed

+744
-2
lines changed

11 files changed

+744
-2
lines changed

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

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProcessor;
2727
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchies;
2828
import org.springframework.ide.vscode.boot.java.beans.DependsOnCompletionProcessor;
29+
import org.springframework.ide.vscode.boot.java.beans.NamedCompletionProvider;
2930
import org.springframework.ide.vscode.boot.java.beans.ProfileCompletionProvider;
3031
import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider;
3132
import org.springframework.ide.vscode.boot.java.beans.ResourceCompletionProvider;
@@ -127,6 +128,9 @@ BootJavaCompletionEngine javaCompletionEngine(
127128
providers.put(Annotations.RESOURCE_JAVAX, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("name", new ResourceCompletionProvider(springIndex))));
128129
providers.put(Annotations.RESOURCE_JAKARTA, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("name", new ResourceCompletionProvider(springIndex))));
129130

131+
providers.put(Annotations.NAMED_JAKARTA, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new NamedCompletionProvider(springIndex))));
132+
providers.put(Annotations.NAMED_JAVAX, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new NamedCompletionProvider(springIndex))));
133+
130134
return new BootJavaCompletionEngine(cuCache, providers, snippetManager);
131135
}
132136

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

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.ide.vscode.boot.index.cache.IndexCacheVoid;
4747
import org.springframework.ide.vscode.boot.java.JavaDefinitionHandler;
4848
import org.springframework.ide.vscode.boot.java.beans.DependsOnDefinitionProvider;
49+
import org.springframework.ide.vscode.boot.java.beans.NamedDefinitionProvider;
4950
import org.springframework.ide.vscode.boot.java.beans.QualifierDefinitionProvider;
5051
import org.springframework.ide.vscode.boot.java.beans.ResourceDefinitionProvider;
5152
import org.springframework.ide.vscode.boot.java.data.jpa.queries.DataQueryParameterDefinitionProvider;
@@ -402,6 +403,7 @@ JavaDefinitionHandler javaDefinitionHandler(SimpleLanguageServer server, Compila
402403
new DependsOnDefinitionProvider(springIndex),
403404
new ResourceDefinitionProvider(springIndex),
404405
new QualifierDefinitionProvider(springIndex),
406+
new NamedDefinitionProvider(springIndex),
405407
new DataQueryParameterDefinitionProvider(server.getTextDocumentService(), qurySemanticTokens)));
406408
}
407409

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

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
package org.springframework.ide.vscode.boot.java;
1212

1313
import java.util.Map;
14+
import java.util.Set;
1415

1516
/**
1617
* Constants containing various fully-qualified annotation names.
@@ -82,8 +83,10 @@ public class Annotations {
8283

8384
public static final String INJECT_JAVAX = "javax.inject.Inject";
8485
public static final String INJECT_JAKARTA = "jakarta.inject.Inject";
86+
8587
public static final String NAMED_JAVAX = "javax.inject.Named";
8688
public static final String NAMED_JAKARTA = "jakarta.inject.Named";
89+
public static final Set<String> NAMED_ANNOTATIONS = Set.of(Annotations.NAMED_JAKARTA, Annotations.NAMED_JAVAX);
8790

8891
public static final String SCHEDULED = "org.springframework.scheduling.annotation.Scheduled";
8992

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

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
2828
import org.springframework.ide.vscode.boot.java.annotations.AnnotationHierarchyAwareLookup;
2929
import org.springframework.ide.vscode.boot.java.autowired.AutowiredHoverProvider;
30+
import org.springframework.ide.vscode.boot.java.beans.NamedReferencesProvider;
3031
import org.springframework.ide.vscode.boot.java.beans.ProfileReferencesProvider;
3132
import org.springframework.ide.vscode.boot.java.beans.QualifierReferencesProvider;
3233
import org.springframework.ide.vscode.boot.java.conditionals.ConditionalsLiveHoverProvider;
@@ -308,9 +309,13 @@ protected BootJavaHoverProvider createHoverHandler(JavaProjectFinder javaProject
308309

309310
protected ReferencesHandler createReferenceHandler(SimpleLanguageServer server, JavaProjectFinder projectFinder,
310311
SpringMetamodelIndex index, SpringSymbolIndex symbolIndex, CompilationUnitCache cuCache) {
312+
311313
Map<String, ReferenceProvider> providers = new HashMap<>();
314+
312315
providers.put(Annotations.VALUE, new ValuePropertyReferencesProvider(server));
313316
providers.put(Annotations.QUALIFIER, new QualifierReferencesProvider(index, symbolIndex));
317+
providers.put(Annotations.NAMED_JAKARTA, new NamedReferencesProvider(index, symbolIndex));
318+
providers.put(Annotations.NAMED_JAVAX, new NamedReferencesProvider(index, symbolIndex));
314319
providers.put(Annotations.PROFILE, new ProfileReferencesProvider(index, symbolIndex));
315320

316321
return new BootJavaReferencesHandler(this, cuCache, projectFinder, providers);

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@
4848
public class ComponentSymbolProvider extends AbstractSymbolProvider {
4949

5050
private static final Logger log = LoggerFactory.getLogger(ComponentSymbolProvider.class);
51-
private static final Set<String> NAMED_ANNOTATIONS = Set.of(Annotations.NAMED_JAKARTA, Annotations.NAMED_JAVAX);
5251

5352
@Override
5453
protected void addSymbolsPass1(Annotation node, ITypeBinding annotationType, Collection<ITypeBinding> metaAnnotations, SpringIndexerJavaContext context, TextDocument doc) {
@@ -61,7 +60,7 @@ protected void addSymbolsPass1(Annotation node, ITypeBinding annotationType, Col
6160
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol));
6261
context.getBeans().add(new CachedBean(context.getDocURI(), beanDefinition));
6362
}
64-
else if (NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) {
63+
else if (Annotations.NAMED_ANNOTATIONS.contains(annotationType.getQualifiedName())) {
6564
WorkspaceSymbol symbol = DefaultSymbolProvider.provideDefaultSymbol(node, doc);
6665
EnhancedSymbolInformation enhancedSymbol = new EnhancedSymbolInformation(symbol, null);
6766
context.getGeneratedSymbols().add(new CachedSymbol(context.getDocURI(), context.getLastModified(), enhancedSymbol));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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.List;
15+
import java.util.stream.Stream;
16+
17+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
18+
import org.springframework.ide.vscode.boot.java.Annotations;
19+
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider;
20+
import org.springframework.ide.vscode.commons.java.IJavaProject;
21+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
22+
23+
/**
24+
* @author Martin Lippert
25+
*/
26+
public class NamedCompletionProvider implements AnnotationAttributeCompletionProvider {
27+
28+
private final SpringMetamodelIndex springIndex;
29+
30+
public NamedCompletionProvider(SpringMetamodelIndex springIndex) {
31+
this.springIndex = springIndex;
32+
}
33+
34+
@Override
35+
public List<String> getCompletionCandidates(IJavaProject project) {
36+
37+
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
38+
39+
return Stream.concat(
40+
findAllNamedValues(beans),
41+
Arrays.stream(beans).map(bean -> bean.getName()))
42+
.distinct()
43+
.toList();
44+
}
45+
46+
private Stream<String> findAllNamedValues(Bean[] beans) {
47+
48+
Stream<String> qualifiersFromBeans = Arrays.stream(beans)
49+
// annotations from beans themselves
50+
.flatMap(bean -> Arrays.stream(bean.getAnnotations()))
51+
.filter(annotation -> Annotations.NAMED_ANNOTATIONS.contains(annotation.getAnnotationType()))
52+
.filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value") && annotation.getAttributes().get("value").length == 1)
53+
.map(annotation -> annotation.getAttributes().get("value")[0]);
54+
55+
Stream<String> qualifiersFromInjectionPoints = Arrays.stream(beans)
56+
// annotations from beans themselves
57+
.filter(bean -> bean.getInjectionPoints() != null)
58+
.flatMap(bean -> Arrays.stream(bean.getInjectionPoints()))
59+
.filter(injectionPoint -> injectionPoint.getAnnotations() != null)
60+
.flatMap(injectionPoint -> Arrays.stream(injectionPoint.getAnnotations()))
61+
.filter(annotation -> Annotations.NAMED_ANNOTATIONS.contains(annotation.getAnnotationType()))
62+
.filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value") && annotation.getAttributes().get("value").length == 1)
63+
.map(annotation -> annotation.getAttributes().get("value")[0]);
64+
65+
return Stream.concat(qualifiersFromBeans, qualifiersFromInjectionPoints);
66+
}
67+
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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.Collections;
15+
import java.util.List;
16+
import java.util.stream.Collectors;
17+
18+
import org.eclipse.jdt.core.dom.ASTNode;
19+
import org.eclipse.jdt.core.dom.Annotation;
20+
import org.eclipse.jdt.core.dom.CompilationUnit;
21+
import org.eclipse.jdt.core.dom.IAnnotationBinding;
22+
import org.eclipse.jdt.core.dom.StringLiteral;
23+
import org.eclipse.lsp4j.LocationLink;
24+
import org.eclipse.lsp4j.TextDocumentIdentifier;
25+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
26+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
27+
import org.springframework.ide.vscode.boot.java.Annotations;
28+
import org.springframework.ide.vscode.boot.java.IJavaDefinitionProvider;
29+
import org.springframework.ide.vscode.boot.java.utils.ASTUtils;
30+
import org.springframework.ide.vscode.commons.java.IJavaProject;
31+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
32+
33+
/**
34+
* @author Martin Lippert
35+
*/
36+
public class NamedDefinitionProvider implements IJavaDefinitionProvider {
37+
38+
private final SpringMetamodelIndex springIndex;
39+
40+
public NamedDefinitionProvider(SpringMetamodelIndex springIndex) {
41+
this.springIndex = springIndex;
42+
}
43+
44+
@Override
45+
public List<LocationLink> getDefinitions(CancelChecker cancelToken, IJavaProject project, TextDocumentIdentifier docId, CompilationUnit cu, ASTNode n, int offset) {
46+
if (n instanceof StringLiteral) {
47+
StringLiteral valueNode = (StringLiteral) n;
48+
49+
ASTNode parent = ASTUtils.getNearestAnnotationParent(valueNode);
50+
51+
if (parent != null && parent instanceof Annotation) {
52+
Annotation a = (Annotation) parent;
53+
IAnnotationBinding binding = a.resolveAnnotationBinding();
54+
if (binding != null && binding.getAnnotationType() != null && Annotations.NAMED_ANNOTATIONS.contains(binding.getAnnotationType().getQualifiedName())) {
55+
String beanName = valueNode.getLiteralValue();
56+
57+
if (beanName != null && beanName.length() > 0) {
58+
return findBeansWithName(project, beanName);
59+
}
60+
}
61+
}
62+
}
63+
return Collections.emptyList();
64+
}
65+
66+
private List<LocationLink> findBeansWithName(IJavaProject project, String beanName) {
67+
Bean[] beans = this.springIndex.getBeansWithName(project.getElementName(), beanName);
68+
69+
return Arrays.stream(beans)
70+
.map(bean -> {
71+
return new LocationLink(bean.getLocation().getUri(), bean.getLocation().getRange(), bean.getLocation().getRange());
72+
})
73+
.collect(Collectors.toList());
74+
}
75+
76+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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.List;
15+
import java.util.stream.Stream;
16+
17+
import org.eclipse.jdt.core.dom.ASTNode;
18+
import org.eclipse.jdt.core.dom.Annotation;
19+
import org.eclipse.jdt.core.dom.ITypeBinding;
20+
import org.eclipse.jdt.core.dom.MemberValuePair;
21+
import org.eclipse.jdt.core.dom.StringLiteral;
22+
import org.eclipse.lsp4j.Location;
23+
import org.eclipse.lsp4j.WorkspaceSymbol;
24+
import org.eclipse.lsp4j.jsonrpc.CancelChecker;
25+
import org.springframework.ide.vscode.boot.app.SpringSymbolIndex;
26+
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
27+
import org.springframework.ide.vscode.boot.java.handlers.ReferenceProvider;
28+
import org.springframework.ide.vscode.commons.java.IJavaProject;
29+
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
30+
31+
/**
32+
* @author Martin Lippert
33+
*/
34+
public class NamedReferencesProvider implements ReferenceProvider {
35+
36+
private final SpringMetamodelIndex springIndex;
37+
private final SpringSymbolIndex symbolIndex;
38+
39+
public NamedReferencesProvider(SpringMetamodelIndex springIndex, SpringSymbolIndex symbolIndex) {
40+
this.springIndex = springIndex;
41+
this.symbolIndex = symbolIndex;
42+
}
43+
44+
@Override
45+
public List<? extends Location> provideReferences(CancelChecker cancelToken, IJavaProject project, ASTNode node, Annotation annotation, ITypeBinding type, int offset) {
46+
47+
cancelToken.checkCanceled();
48+
49+
try {
50+
// case: @Value("prefix<*>")
51+
if (node instanceof StringLiteral && node.getParent() instanceof Annotation) {
52+
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
53+
return provideReferences(project, ((StringLiteral) node).getLiteralValue());
54+
}
55+
}
56+
// case: @Value(value="prefix<*>")
57+
else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair
58+
&& "value".equals(((MemberValuePair)node.getParent()).getName().toString())) {
59+
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
60+
return provideReferences(project, ((StringLiteral) node).getLiteralValue());
61+
}
62+
}
63+
}
64+
catch (Exception e) {
65+
e.printStackTrace();
66+
}
67+
68+
return null;
69+
}
70+
71+
private List<? extends Location> provideReferences(IJavaProject project, String value) {
72+
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
73+
74+
// beans with name
75+
Stream<Location> beanLocations = Arrays.stream(beans)
76+
.filter(bean -> bean.getName().equals(value))
77+
.map(bean -> bean.getLocation());
78+
79+
String exactPhrase1 = "@Named(\"" + value + "\")";
80+
String exactPhrase2 = "@Named(value=\"" + value + "\")";
81+
82+
// qualifier annotations
83+
List<WorkspaceSymbol> qualifierSymbols1 = symbolIndex.getAllSymbols(exactPhrase1);
84+
List<WorkspaceSymbol> qualifierSymbols2 = symbolIndex.getAllSymbols(exactPhrase2);
85+
86+
Stream<Location> qualifierLocations = Stream.concat(qualifierSymbols1.stream(), qualifierSymbols2.stream())
87+
.filter(symbol -> symbol.getName().contains(exactPhrase1) || symbol.getName().contains(exactPhrase2))
88+
.map(symbol -> symbol.getLocation())
89+
.filter(location -> location.isLeft())
90+
.map(location -> location.getLeft());
91+
92+
return Stream.concat(qualifierLocations, beanLocations).toList();
93+
}
94+
95+
}

0 commit comments

Comments
 (0)