Skip to content

Commit

Permalink
GH-1285: add support for code completion for classpath resources in v…
Browse files Browse the repository at this point in the history
…alue annotation
  • Loading branch information
martinlippert committed Jul 3, 2024
1 parent ab23402 commit 32671ae
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017, 2019 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
Expand All @@ -22,13 +22,18 @@
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.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.StringLiteral;
import org.eclipse.lsp4j.TextDocumentIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ide.vscode.boot.java.beans.QualifierCompletionProposal;
import org.springframework.ide.vscode.boot.java.handlers.CompletionProvider;
import org.springframework.ide.vscode.boot.metadata.ProjectBasedPropertyIndexProvider;
import org.springframework.ide.vscode.boot.metadata.PropertyInfo;
import org.springframework.ide.vscode.boot.metadata.SpringPropertyIndexProvider;
import org.springframework.ide.vscode.commons.java.IClasspathUtil;
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;
Expand All @@ -43,6 +48,8 @@
* @author Martin Lippert
*/
public class ValueCompletionProcessor implements CompletionProvider {

private static final Logger log = LoggerFactory.getLogger(ValueCompletionProcessor.class);

private final SpringPropertyIndexProvider indexProvider;
private final ProjectBasedPropertyIndexProvider adHocIndexProvider;
Expand All @@ -59,6 +66,13 @@ public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding
int offset, TextDocument doc, Collection<ICompletionProposal> completions) {

try {
Optional<IJavaProject> optionalProject = this.projectFinder.find(doc.getId());
if (optionalProject.isEmpty()) {
return;
}

IJavaProject project = optionalProject.get();

// case: @Value(<*>)
if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) {
List<Match<PropertyInfo>> matches = findMatches("", doc);
Expand All @@ -75,37 +89,70 @@ public void provideCompletions(ASTNode node, Annotation annotation, ITypeBinding

completions.add(proposal);
}

addClasspathResourceProposals(project, doc, offset, offset, "", true, completions);
}
// case: @Value(prefix<*>)
else if (node instanceof SimpleName && node.getParent() instanceof Annotation) {
computeProposalsForSimpleName(node, completions, offset, doc);
computeProposalsForSimpleName(project, node, completions, offset, doc);
}
// case: @Value(file.ext<*>) - the "." causes a QualifierNode to be generated
else if (node instanceof SimpleName && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof Annotation) {
computeProposalsForSimpleName(project, node.getParent(), completions, offset, doc);
}
// case: @Value(value=<*>)
else if (node instanceof SimpleName && node.getParent() instanceof MemberValuePair
&& "value".equals(((MemberValuePair)node.getParent()).getName().toString())) {
computeProposalsForSimpleName(node, completions, offset, doc);
computeProposalsForSimpleName(project, node, completions, offset, doc);
}
// case: @Value(value=<*>)
else if (node instanceof SimpleName && node.getParent() instanceof QualifiedName && node.getParent().getParent() instanceof MemberValuePair
&& "value".equals(((MemberValuePair)node.getParent().getParent()).getName().toString())) {
computeProposalsForSimpleName(project, node.getParent(), completions, offset, doc);
}
// case: @Value("prefix<*>")
else if (node instanceof StringLiteral && node.getParent() instanceof Annotation) {
if (node.toString().startsWith("\"") && node.toString().endsWith("\"")) {
computeProposalsForStringLiteral(node, completions, offset, doc);
computeProposalsForStringLiteral(project, (StringLiteral) node, completions, offset, doc);
}
}
// case: @Value(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(node, completions, offset, doc);
computeProposalsForStringLiteral(project, (StringLiteral) node, completions, offset, doc);
}
}
}
catch (Exception e) {
e.printStackTrace();
log.error("problem while looking for value annotation proposals", e);
}
}

private void addClasspathResourceProposals(IJavaProject project, TextDocument doc, int startOffset, int endOffset, String prefix, boolean includeQuotes, Collection<ICompletionProposal> completions) {
String[] resources = findResources(project, prefix);

double score = resources.length + 1000;
for (String resource : resources) {

DocumentEdits edits = new DocumentEdits(doc, false);

if (includeQuotes) {
edits.replace(startOffset, endOffset, "\"classpath:" + resource + "\"");
}
else {
edits.replace(startOffset, endOffset, "classpath:" + resource);
}

String label = "classpath:" + resource;

ICompletionProposal proposal = new QualifierCompletionProposal(edits, label, label, null, score--);
completions.add(proposal);
}

}

private void computeProposalsForSimpleName(ASTNode node, Collection<ICompletionProposal> completions, int offset,
IDocument doc) {
private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) {
String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition());

int startOffset = node.getStartPosition();
Expand All @@ -125,10 +172,12 @@ private void computeProposalsForSimpleName(ASTNode node, Collection<ICompletionP

completions.add(proposal);
}

String unfilteredPrefix = node.toString().substring(0, offset - node.getStartPosition());
addClasspathResourceProposals(project, doc, startOffset, endOffset, unfilteredPrefix, true, completions);
}

private void computeProposalsForStringLiteral(ASTNode node, Collection<ICompletionProposal> completions, int offset,
IDocument doc) throws BadLocationException {
private void computeProposalsForStringLiteral(IJavaProject project, StringLiteral node, Collection<ICompletionProposal> completions, int offset, TextDocument doc) throws BadLocationException {
String prefix = identifyPropertyPrefix(doc.get(node.getStartPosition() + 1, offset - (node.getStartPosition() + 1)), offset - (node.getStartPosition() + 1));

int startOffset = offset - prefix.length();
Expand Down Expand Up @@ -164,6 +213,9 @@ else if (prePrefix.endsWith("$")) {

completions.add(proposal);
}

String unfilteredPrefix = node.getLiteralValue().substring(0, offset - (node.getStartPosition() + 1));
addClasspathResourceProposals(project, doc, startOffset, endOffset, unfilteredPrefix, false, completions);
}

private boolean isClosingBracketMissing(String fullNodeContent) {
Expand Down Expand Up @@ -220,4 +272,14 @@ private List<Match<PropertyInfo>> findMatches(String prefix, IDocument doc) {
return matches;
}

private String[] findResources(IJavaProject project, String prefix) {
String[] resources = IClasspathUtil.getClasspathResources(project.getClasspath()).stream()
.distinct()
.map(r -> r.replaceAll("\\\\", "/"))
.filter(r -> ("classpath:" + r).contains(prefix))
.toArray(String[]::new);

return resources;
}

}
Loading

0 comments on commit 32671ae

Please sign in to comment.