GH-1261: extracted general annotation attribute content-assist mechanics, first steps towards a more re-usaeble implementation

This commit is contained in:
Martin Lippert
2024-06-28 08:58:27 +02:00
parent 0e64d96836
commit 7d44b48c4b
11 changed files with 463 additions and 407 deletions

View File

@@ -23,9 +23,10 @@ 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.AnnotationAttributeCompletionProcessor;
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.beans.QualifierCompletionProcessor;
import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProvider;
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;
@@ -111,12 +112,14 @@ public class BootJavaCompletionEngineConfigurer {
Map<String, CompletionProvider> providers = new HashMap<>();
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.QUALIFIER, new QualifierCompletionProcessor(javaProjectFinder, springIndex));
providers.put(Annotations.REPOSITORY, new DataRepositoryCompletionProcessor());
providers.put(Annotations.SCOPE, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new ScopeCompletionProcessor())));
providers.put(Annotations.DEPENDS_ON, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new DependsOnCompletionProcessor(springIndex))));
providers.put(Annotations.QUALIFIER, new AnnotationAttributeCompletionProcessor(javaProjectFinder, Map.of("value", new QualifierCompletionProvider(springIndex))));
return new BootJavaCompletionEngine(cuCache, providers, snippetManager);
}

View File

@@ -8,18 +8,16 @@
* Contributors:
* Broadcom - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
package org.springframework.ide.vscode.boot.java.annotations;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
@@ -28,28 +26,26 @@ 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.Annotations;
import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProposal;
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.TextDocument;
/**
* @author Martin Lippert
*/
public class QualifierCompletionProcessor implements CompletionProvider {
public class AnnotationAttributeCompletionProcessor implements CompletionProvider {
private final JavaProjectFinder projectFinder;
private final SpringMetamodelIndex springIndex;
private final Map<String, AnnotationAttributeCompletionProvider> completionProviders;
public QualifierCompletionProcessor(JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) {
public AnnotationAttributeCompletionProcessor(JavaProjectFinder projectFinder, Map<String, AnnotationAttributeCompletionProvider> completionProviders) {
this.projectFinder = projectFinder;
this.springIndex = springIndex;
this.completionProviders = completionProviders;
}
@Override
@@ -67,7 +63,6 @@ public class QualifierCompletionProcessor implements CompletionProvider {
// case: @Qualifier(<*>)
if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) {
createCompletionProposals(project, doc, node, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\"");
}
// case: @Qualifier(prefix<*>)
else if (node instanceof SimpleName && node.getParent() instanceof Annotation) {
@@ -105,7 +100,41 @@ public class QualifierCompletionProcessor implements CompletionProvider {
e.printStackTrace();
}
}
/**
* create the concrete completion proposal
*/
private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, Collection<ICompletionProposal> completions, int startOffset, int endOffset,
String filterPrefix, Function<String, String> createReplacementText) {
Set<String> alreadyMentionedValues = alreadyMentionedValues(node);
AnnotationAttributeCompletionProvider completionProvider = this.completionProviders.get("value");
if (completionProvider != null) {
List<String> candidates = completionProvider.getCompletionCandidates(project);
List<String> filteredCandidates = candidates.stream()
.filter(candidate -> candidate.toLowerCase().startsWith(filterPrefix.toLowerCase()))
.filter(candidate -> !alreadyMentionedValues.contains(candidate))
.collect(Collectors.toList());
double score = filteredCandidates.size();
for (String candidate : filteredCandidates) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(startOffset, endOffset, createReplacementText.apply(candidate));
QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, candidate, candidate, null, score--);
completions.add(proposal);
}
}
}
//
// internal computation of the right positions, prefixes, etc.
//
private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) {
String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition());
@@ -123,7 +152,7 @@ public class QualifierCompletionProcessor implements CompletionProvider {
String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, length), length);
int startOffset = offset - prefix.length();
int endOffset = offset;
int endOffset = node.getStartPosition() + node.getLength() - 1;
createCompletionProposals(project, doc, node, completions, startOffset, endOffset, prefix, (beanName) -> beanName);
}
@@ -142,34 +171,6 @@ public class QualifierCompletionProcessor implements CompletionProvider {
}
}
private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, Collection<ICompletionProposal> completions, int startOffset, int endOffset,
String filterPrefix, Function<String, String> createReplacementText) {
Set<String> mentionedQualifiers = alreadyMentionedValues(node);
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
Set<String> candidates = Stream.concat(
findAllQualifiers(beans),
Arrays.stream(beans).map(bean -> bean.getName()))
.collect(Collectors.toCollection(LinkedHashSet::new));
List<String> filteredCandidates = candidates.stream()
.filter(candidate -> candidate.toLowerCase().startsWith(filterPrefix.toLowerCase()))
.filter(candidate -> !mentionedQualifiers.contains(candidate))
.collect(Collectors.toList());
double score = filteredCandidates.size();
for (String candidate : filteredCandidates) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(startOffset, endOffset, createReplacementText.apply(candidate));
QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, candidate, candidate, null, score--);
completions.add(proposal);
}
}
private String identifyPropertyPrefix(String nodeContent, int offset) {
String result = nodeContent.substring(0, offset);
@@ -186,7 +187,7 @@ public class QualifierCompletionProcessor implements CompletionProvider {
return result;
}
private Set<String> alreadyMentionedValues(ASTNode node) {
protected Set<String> alreadyMentionedValues(ASTNode node) {
Set<String> result = new HashSet<>();
ArrayInitializer arrayNode = null;
@@ -213,29 +214,4 @@ public class QualifierCompletionProcessor implements CompletionProvider {
return result;
}
private Stream<String> findAllQualifiers(Bean[] beans) {
Stream<String> qualifiersFromBeans = Arrays.stream(beans)
// annotations from beans themselves
.flatMap(bean -> Arrays.stream(bean.getAnnotations()))
.filter(annotation -> Annotations.QUALIFIER.equals(annotation.getAnnotationType()))
.filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value") && annotation.getAttributes().get("value").length == 1)
.map(annotation -> annotation.getAttributes().get("value")[0]);
Stream<String> qualifiersFromInjectionPoints = Arrays.stream(beans)
// annotations from beans themselves
.filter(bean -> bean.getInjectionPoints() != null)
.flatMap(bean -> Arrays.stream(bean.getInjectionPoints()))
.filter(injectionPoint -> injectionPoint.getAnnotations() != null)
.flatMap(injectionPoint -> Arrays.stream(injectionPoint.getAnnotations()))
.filter(annotation -> Annotations.QUALIFIER.equals(annotation.getAnnotationType()))
.filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value") && annotation.getAttributes().get("value").length == 1)
.map(annotation -> annotation.getAttributes().get("value")[0]);
return Stream.concat(qualifiersFromBeans, qualifiersFromInjectionPoints);
}
}

View File

@@ -1,18 +1,21 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* 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:
* Pivotal, Inc. - initial API and implementation
* Broadcom - initial API and implementation
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.requestmapping;
package org.springframework.ide.vscode.boot.java.annotations;
/**
* @author Martin Lippert
*/
public class RequestMappingCompletionProcessor {
import java.util.List;
import org.springframework.ide.vscode.commons.java.IJavaProject;
public interface AnnotationAttributeCompletionProvider {
List<String> getCompletionCandidates(IJavaProject project);
}

View File

@@ -11,248 +11,235 @@
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.boot.java.annotations.AnnotationAttributeCompletionProvider;
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 {
public class DependsOnCompletionProcessor implements AnnotationAttributeCompletionProvider {
private final JavaProjectFinder projectFinder;
private final SpringMetamodelIndex springIndex;
public DependsOnCompletionProcessor(JavaProjectFinder projectFinder, SpringMetamodelIndex springIndex) {
this.projectFinder = projectFinder;
public DependsOnCompletionProcessor(SpringMetamodelIndex springIndex) {
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() + "\"");
//
// 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);
//
// 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());
//
// 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() + "\"");
//
// 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() + "\",");
//
// 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;
// }
@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() + "\"");
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);
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());
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() + "\"");
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() + "\",");
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;
public List<String> getCompletionCandidates(IJavaProject project) {
return Arrays.stream(this.springIndex.getBeansOfProject(project.getElementName()))
.map(bean -> bean.getName())
.distinct()
.toList();
}

View File

@@ -0,0 +1,68 @@
/*******************************************************************************
* 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.List;
import java.util.stream.Stream;
import org.springframework.ide.vscode.boot.index.SpringMetamodelIndex;
import org.springframework.ide.vscode.boot.java.Annotations;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider;
import org.springframework.ide.vscode.commons.java.IJavaProject;
import org.springframework.ide.vscode.commons.protocol.spring.Bean;
/**
* @author Martin Lippert
*/
public class QualifierCompletionProvider implements AnnotationAttributeCompletionProvider {
private final SpringMetamodelIndex springIndex;
public QualifierCompletionProvider(SpringMetamodelIndex springIndex) {
this.springIndex = springIndex;
}
@Override
public List<String> getCompletionCandidates(IJavaProject project) {
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
return Stream.concat(
findAllQualifiers(beans),
Arrays.stream(beans).map(bean -> bean.getName()))
.distinct()
.toList();
}
private Stream<String> findAllQualifiers(Bean[] beans) {
Stream<String> qualifiersFromBeans = Arrays.stream(beans)
// annotations from beans themselves
.flatMap(bean -> Arrays.stream(bean.getAnnotations()))
.filter(annotation -> Annotations.QUALIFIER.equals(annotation.getAnnotationType()))
.filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value") && annotation.getAttributes().get("value").length == 1)
.map(annotation -> annotation.getAttributes().get("value")[0]);
Stream<String> qualifiersFromInjectionPoints = Arrays.stream(beans)
// annotations from beans themselves
.filter(bean -> bean.getInjectionPoints() != null)
.flatMap(bean -> Arrays.stream(bean.getInjectionPoints()))
.filter(injectionPoint -> injectionPoint.getAnnotations() != null)
.flatMap(injectionPoint -> Arrays.stream(injectionPoint.getAnnotations()))
.filter(annotation -> Annotations.QUALIFIER.equals(annotation.getAnnotationType()))
.filter(annotation -> annotation.getAttributes() != null && annotation.getAttributes().containsKey("value") && annotation.getAttributes().get("value").length == 1)
.map(annotation -> annotation.getAttributes().get("value")[0]);
return Stream.concat(qualifiersFromBeans, qualifiersFromInjectionPoints);
}
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 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
@@ -10,76 +10,84 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.scope;
import java.util.Collection;
import java.util.List;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Annotation;
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.java.handlers.CompletionProvider;
import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
import org.springframework.ide.vscode.boot.java.annotations.AnnotationAttributeCompletionProvider;
import org.springframework.ide.vscode.commons.java.IJavaProject;
/**
* @author Martin Lippert
*/
public class ScopeCompletionProcessor implements CompletionProvider {
public class ScopeCompletionProcessor implements AnnotationAttributeCompletionProvider {
private static final List<String> SCOPE_COMPLETIONS = List.of(
"application",
"globalSession",
"prototype",
"request",
"session",
"singleton",
"websocket"
);
@Override
public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type,
int offset, TextDocument doc, Collection<ICompletionProposal> completions) {
try {
if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair) {
MemberValuePair memberPair = (MemberValuePair) node.getParent();
// case: @Scope(value=<*>)
if ("value".equals(memberPair.getName().toString()) && memberPair.getValue().toString().equals("$missing$")) {
for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) {
ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, offset, offset, "");
completions.add(proposal);
}
}
}
// case: @Scope(<*>)
else if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) {
for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) {
ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, offset, offset, "");
completions.add(proposal);
}
}
else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) {
// case: @Scope("...")
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
String prefix = doc.get(node.getStartPosition(), offset - node.getStartPosition());
for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) {
if (completion.getValue().startsWith(prefix)) {
ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, node.getStartPosition(), node.getStartPosition() + node.getLength(), prefix);
completions.add(proposal);
}
}
}
}
else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair) {
MemberValuePair memberPair = (MemberValuePair) node.getParent();
// case: @Scope(value=<*>)
if ("value".equals(memberPair.getName().toString()) && node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
String prefix = doc.get(node.getStartPosition(), offset - node.getStartPosition());
for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) {
if (completion.getValue().startsWith(prefix)) {
ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, node.getStartPosition(), node.getStartPosition() + node.getLength(), prefix);
completions.add(proposal);
}
}
}
}
}
catch (Exception e) {
e.printStackTrace();
}
public List<String> getCompletionCandidates(IJavaProject project) {
return SCOPE_COMPLETIONS;
}
// @Override
// public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding type,
// int offset, TextDocument doc, Collection<ICompletionProposal> completions) {
//
// try {
// if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair) {
// MemberValuePair memberPair = (MemberValuePair) node.getParent();
//
// // case: @Scope(value=<*>)
// if ("value".equals(memberPair.getName().toString()) && memberPair.getValue().toString().equals("$missing$")) {
// for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) {
// ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, offset, offset, "");
// completions.add(proposal);
// }
// }
// }
// // case: @Scope(<*>)
// else if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) {
// for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) {
// ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, offset, offset, "");
// completions.add(proposal);
// }
// }
// else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) {
// // case: @Scope("...")
// if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
// String prefix = doc.get(node.getStartPosition(), offset - node.getStartPosition());
// for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) {
// if (completion.getValue().startsWith(prefix)) {
// ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, node.getStartPosition(), node.getStartPosition() + node.getLength(), prefix);
// completions.add(proposal);
// }
// }
// }
// }
// else if (node instanceof StringLiteral && node.getParent() instanceof MemberValuePair) {
// MemberValuePair memberPair = (MemberValuePair) node.getParent();
//
// // case: @Scope(value=<*>)
// if ("value".equals(memberPair.getName().toString()) && node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
// String prefix = doc.get(node.getStartPosition(), offset - node.getStartPosition());
// for (ScopeNameCompletion completion : ScopeNameCompletionProposal.COMPLETIONS) {
// if (completion.getValue().startsWith(prefix)) {
// ICompletionProposal proposal = new ScopeNameCompletionProposal(completion, doc, node.getStartPosition(), node.getStartPosition() + node.getLength(), prefix);
// completions.add(proposal);
// }
// }
// }
// }
// }
// catch (Exception e) {
// e.printStackTrace();
// }
// }
}

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017, 2023 Pivotal, Inc.
* Copyright (c) 2017, 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
@@ -46,9 +46,9 @@ public class PropertyIndexHarness {
@Override
public SpringPropertyIndex getIndex(IDocument doc) {
synchronized (PropertyIndexHarness.this) {
if (index==null) {
if (index == null) {
IClasspath classpath = testProject == null ? null : testProject.getClasspath();
index = SpringPropertyIndex.builder(valueProviders).withClasspath(classpath).build();
index = SpringPropertyIndex.builder(valueProviders).withClasspath(classpath).build();
for (ConfigurationMetadataProperty propertyInfo : datas.values()) {
index.add(propertyInfo);
}

View File

@@ -92,6 +92,12 @@ public class DependsOnCompletionProviderTest {
assertCompletions("@DependsOn(<*>)", 2, "@DependsOn(\"bean1\"<*>)");
}
// TODO: not yet working, needs more groundwork due to the parser skipping these non-valid parts of the AST
// @Test
// public void testDependsOnCompletionWithoutQuotesWithoutPrefixWithoutClosingBracket() throws Exception {
// assertCompletions("@DependsOn(<*>", 2, "@DependsOn(\"bean1\")<*>");
// }
@Test
public void testDependsOnCompletionWithoutQuotesWithPrefix() throws Exception {
assertCompletions("@DependsOn(be<*>)", 2, "@DependsOn(\"bean1\"<*>)");
@@ -102,11 +108,23 @@ public class DependsOnCompletionProviderTest {
assertCompletions("@DependsOn(value=<*>)", 2, "@DependsOn(value=\"bean1\"<*>)");
}
// TODO: not yet working, needs more groundwork due to the parser skipping these non-valid parts of the AST
// @Test
// public void testDependsOnCompletionWithoutQuotesWithAttributeNameAndDefaultSpaces() throws Exception {
// assertCompletions("@DependsOn(value = <*>)", 2, "@DependsOn(value = \"bean1\"<*>)");
// }
@Test
public void testDependsOnCompletionInsideOfQuotesWithoutPrefix() throws Exception {
assertCompletions("@DependsOn(\"<*>\")", 2, "@DependsOn(\"bean1<*>\")");
}
// TODO: not yet working, needs more groundwork due to the parser skipping these non-valid parts of the AST
// @Test
// public void testDependsOnCompletionOpeningQuoteOnlyWithoutPrefix() throws Exception {
// assertCompletions("@DependsOn(\"<*>)", 2, "@DependsOn(\"bean1<*>\")");
// }
@Test
public void testDependsOnCompletionWithoutQuotesWithoutPrefixInsideArray() throws Exception {
assertCompletions("@DependsOn({<*>})", 2, "@DependsOn({\"bean1\"<*>})");
@@ -144,7 +162,7 @@ public class DependsOnCompletionProviderTest {
@Test
public void testDependsOnCompletionInsideOfQuotesWithPrefixAndReplacedPostfix() throws Exception {
assertCompletions("@DependsOn(\"be<*>xxx\")", 2, "@DependsOn(\"bean1<*>xxx\")");
assertCompletions("@DependsOn(\"be<*>xxx\")", 2, "@DependsOn(\"bean1<*>\")");
}
@Test

View File

@@ -140,7 +140,7 @@ public class QualifierCompletionProviderTest {
@Test
public void testQualifierCompletionInsideOfQuotesWithPrefixAndReplacedPostfix() throws Exception {
assertCompletions("@Qualifier(\"be<*>xxx\")", 2, "@Qualifier(\"bean1<*>xxx\")");
assertCompletions("@Qualifier(\"be<*>xxx\")", 2, "@Qualifier(\"bean1<*>\")");
}
private void assertCompletions(String completionLine, int noOfExpectedCompletions, String expectedCompletedLine) throws Exception {

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017 Pivotal, Inc.
* Copyright (c) 2017, 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
@@ -10,12 +10,13 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.scope.test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.List;
import org.apache.commons.io.IOUtils;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.eclipse.lsp4j.CompletionItem;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -50,10 +51,6 @@ public class ScopeCompletionTest {
harness.intialize(null);
}
// private IJavaProject getTestProject() {
// return testProject;
// }
@Test
void testEmptyBracketsCompletion() throws Exception {
prepareCase("@Scope(\"onClass\")", "@Scope(<*>)");
@@ -71,13 +68,13 @@ public class ScopeCompletionTest {
void testEmptyStringLiteralCompletion() throws Exception {
prepareCase("@Scope(\"onClass\")", "@Scope(\"<*>\")");
assertAnnotationCompletions(
"@Scope(\"application\"<*>)",
"@Scope(\"globalSession\"<*>)",
"@Scope(\"prototype\"<*>)",
"@Scope(\"request\"<*>)",
"@Scope(\"session\"<*>)",
"@Scope(\"singleton\"<*>)",
"@Scope(\"websocket\"<*>)");
"@Scope(\"application<*>\")",
"@Scope(\"globalSession<*>\")",
"@Scope(\"prototype<*>\")",
"@Scope(\"request<*>\")",
"@Scope(\"session<*>\")",
"@Scope(\"singleton<*>\")",
"@Scope(\"websocket<*>\")");
}
@Test
@@ -97,20 +94,20 @@ public class ScopeCompletionTest {
void testEmptyValueStringLiteralCompletion() throws Exception {
prepareCase("@Scope(\"onClass\")", "@Scope(value=\"<*>\")");
assertAnnotationCompletions(
"@Scope(value=\"application\"<*>)",
"@Scope(value=\"globalSession\"<*>)",
"@Scope(value=\"prototype\"<*>)",
"@Scope(value=\"request\"<*>)",
"@Scope(value=\"session\"<*>)",
"@Scope(value=\"singleton\"<*>)",
"@Scope(value=\"websocket\"<*>)");
"@Scope(value=\"application<*>\")",
"@Scope(value=\"globalSession<*>\")",
"@Scope(value=\"prototype<*>\")",
"@Scope(value=\"request<*>\")",
"@Scope(value=\"session<*>\")",
"@Scope(value=\"singleton<*>\")",
"@Scope(value=\"websocket<*>\")");
}
@Test
void testPrefixWithClosingQuotesCompletion() throws Exception {
prepareCase("@Scope(\"onClass\")", "@Scope(\"pro<*>\")");
assertAnnotationCompletions(
"@Scope(\"prototype\"<*>)");
"@Scope(\"prototype<*>\")");
}
@Test
@@ -123,7 +120,7 @@ public class ScopeCompletionTest {
void testValuePrefixWithClosingQuotesCompletion() throws Exception {
prepareCase("@Scope(\"onClass\")", "@Scope(value=\"pro<*>\")");
assertAnnotationCompletions(
"@Scope(value=\"prototype\"<*>)");
"@Scope(value=\"prototype<*>\")");
}
@Test
@@ -136,7 +133,7 @@ public class ScopeCompletionTest {
void testPrefixReplaceRestCompletion() throws Exception {
prepareCase("@Scope(\"onClass\")", "@Scope(\"pro<*>something\")");
assertAnnotationCompletions(
"@Scope(\"prototype\"<*>)");
"@Scope(\"prototype<*>\")");
}
@Test
@@ -147,7 +144,7 @@ public class ScopeCompletionTest {
private void prepareCase(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception {
InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-annotations/src/main/java/org/test/TestScopeCompletion.java");
String content = IOUtils.toString(resource);
String content = IOUtils.toString(resource, Charset.defaultCharset());
content = content.replace(selectedAnnotation, annotationStatementBeforeTest);
editor = new Editor(harness, content, LanguageId.JAVA);

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017, 2020 Pivotal, Inc.
* Copyright (c) 2017, 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
@@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -63,7 +64,6 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
public class ValueCompletionTest {
@Autowired private BootLanguageServerHarness harness;
@Autowired private IJavaProject testProject;
@Autowired private JavaProjectFinder projectFinder;
private Editor editor;
@@ -132,10 +132,6 @@ public class ValueCompletionTest {
harness.intialize(null);
}
private IJavaProject getTestProject() {
return testProject;
}
@Test
void testPrefixIdentification() {
ValueCompletionProcessor processor = new ValueCompletionProcessor(projectFinder, null, null);
@@ -392,7 +388,7 @@ public class ValueCompletionTest {
private void prepareCase(String selectedAnnotation, String annotationStatementBeforeTest) throws Exception {
InputStream resource = this.getClass().getResourceAsStream("/test-projects/test-annotations/src/main/java/org/test/TestValueCompletion.java");
String content = IOUtils.toString(resource);
String content = IOUtils.toString(resource, Charset.defaultCharset());
content = content.replace(selectedAnnotation, annotationStatementBeforeTest);
editor = new Editor(harness, content, LanguageId.JAVA);