From 19058d935c98a1d12ca3bd0a330bc4dde5a2044d Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Thu, 13 Jun 2024 14:47:14 +0200 Subject: [PATCH] GH-1261: added qualifiers and ranking of proposals to the qualifier completion proposals --- .../beans/QualifierCompletionProcessor.java | 158 +++++++----------- .../beans/QualifierCompletionProposal.java | 21 ++- .../test/QualifierCompletionProviderTest.java | 2 +- 3 files changed, 79 insertions(+), 102 deletions(-) diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProcessor.java index bb2b52e09..cfdea87ea 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProcessor.java @@ -13,10 +13,13 @@ package org.springframework.ide.vscode.boot.java.beans; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; 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; @@ -26,6 +29,7 @@ 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.handlers.CompletionProvider; import org.springframework.ide.vscode.commons.java.IJavaProject; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; @@ -33,7 +37,6 @@ import org.springframework.ide.vscode.commons.languageserver.completion.IComplet 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; /** @@ -63,20 +66,8 @@ public class QualifierCompletionProcessor implements CompletionProvider { // case: @Qualifier(<*>) if (node == annotation && doc.get(offset - 1, 2).endsWith("()")) { - Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + createCompletionProposals(project, doc, node, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\""); - for (Bean bean : beans) { - - DocumentEdits edits = new DocumentEdits(doc, false); - edits.replace(offset, offset, "\"" + bean.getName() + "\""); - - // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet - // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string - // if it cannot resolve it. If sending this as plain text, then insertion happens correctly - QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); - - completions.add(proposal); - } } // case: @Qualifier(prefix<*>) else if (node instanceof SimpleName && node.getParent() instanceof Annotation) { @@ -115,7 +106,7 @@ public class QualifierCompletionProcessor implements CompletionProvider { } } - private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection completions, int offset, IDocument doc) { + private void computeProposalsForSimpleName(IJavaProject project, ASTNode node, Collection completions, int offset, TextDocument doc) { String prefix = identifyPropertyPrefix(node.toString(), offset - node.getStartPosition()); int startOffset = node.getStartPosition(); @@ -123,80 +114,22 @@ public class QualifierCompletionProcessor implements CompletionProvider { String proposalPrefix = "\""; String proposalPostfix = "\""; - - Set mentionedBeans = alreadyMentionedBeans(node); - - Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); - List 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); - - // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet - // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string - // if it cannot resolve it. If sending this as plain text, then insertion happens correctly - QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); - - completions.add(proposal); - } + createCompletionProposals(project, doc, node, completions, startOffset, endOffset, prefix, (beanName) -> proposalPrefix + beanName + proposalPostfix); } - private void computeProposalsForStringLiteral(IJavaProject project, ASTNode node, Collection completions, int offset, IDocument doc) throws BadLocationException { + private void computeProposalsForStringLiteral(IJavaProject project, ASTNode node, Collection completions, int offset, TextDocument 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 mentionedBeans = alreadyMentionedBeans(node); - Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); - - final String filterPrefix = prefix; - List 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()); - - // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet - // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string - // if it cannot resolve it. If sending this as plain text, then insertion happens correctly - QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); - - completions.add(proposal); - } + createCompletionProposals(project, doc, node, completions, startOffset, endOffset, prefix, (beanName) -> beanName); } - private void computeProposalsForArrayInitializr(IJavaProject project, ArrayInitializer node, Collection completions, int offset, IDocument doc) { - Set mentionedBeans = alreadyMentionedBeans(node); - - Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); - List 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() + "\""); - - // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet - // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string - // if it cannot resolve it. If sending this as plain text, then insertion happens correctly - QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); - - completions.add(proposal); - } + private void computeProposalsForArrayInitializr(IJavaProject project, ArrayInitializer node, Collection completions, int offset, TextDocument doc) { + createCompletionProposals(project, doc, node, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\""); } private void computeProposalsForInsideArrayInitializer(IJavaProject project, ASTNode node, Collection completions, int offset, TextDocument doc) throws BadLocationException { @@ -205,25 +138,39 @@ public class QualifierCompletionProcessor implements CompletionProvider { computeProposalsForStringLiteral(project, node, completions, offset, doc); } else { - Set mentionedBeans = alreadyMentionedBeans(node); + createCompletionProposals(project, doc, node, completions, offset, offset, "", (beanName) -> "\"" + beanName + "\","); + } + } - Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); - List filteredBeans = Arrays.stream(beans) - .filter(bean -> !mentionedBeans.contains(bean.getName())) - .collect(Collectors.toList()); - - for (Bean bean : filteredBeans) { + private void createCompletionProposals(IJavaProject project, TextDocument doc, ASTNode node, Collection completions, int startOffset, int endOffset, + String filterPrefix, Function createReplacementText) { - DocumentEdits edits = new DocumentEdits(doc, false); - edits.replace(offset, offset, "\"" + bean.getName() + "\","); + Set mentionedQualifiers = alreadyMentionedValues(node); - // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet - // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string - // if it cannot resolve it. If sending this as plain text, then insertion happens correctly - QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, bean.getName(), bean.getName(), null); + Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName()); + Set candidates = Stream.concat( + findAllQualifiers(beans), + Arrays.stream(beans).map(bean -> bean.getName())) + .collect(Collectors.toCollection(LinkedHashSet::new)); - completions.add(proposal); - } + List 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)); + + // PT-160455522: create a proposal with `PlainText` format type, because for vscode (but not Eclipse), if you send it as a snippet + // and it is "place holder" as such `"${debug}"`, vscode may treat it as a snippet place holder, and insert an empty string + // if it cannot resolve it. If sending this as plain text, then insertion happens correctly + QualifierCompletionProposal proposal = new QualifierCompletionProposal(edits, candidate, candidate, null, score--); + + completions.add(proposal); } } @@ -243,7 +190,7 @@ public class QualifierCompletionProcessor implements CompletionProvider { return result; } - private Set alreadyMentionedBeans(ASTNode node) { + private Set alreadyMentionedValues(ASTNode node) { Set result = new HashSet<>(); ArrayInitializer arrayNode = null; @@ -269,6 +216,29 @@ public class QualifierCompletionProcessor implements CompletionProvider { return result; } + + private Stream findAllQualifiers(Bean[] beans) { + + Stream 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 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); + + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProposal.java index 6eafa90e8..4ca1e990a 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProposal.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/beans/QualifierCompletionProposal.java @@ -12,22 +12,23 @@ package org.springframework.ide.vscode.boot.java.beans; import org.eclipse.lsp4j.CompletionItemKind; 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.completion.ScoreableProposal; import org.springframework.ide.vscode.commons.util.Renderable; /** * @author Martin Lippert */ -public class QualifierCompletionProposal implements ICompletionProposal { +public class QualifierCompletionProposal extends ScoreableProposal { private static final String EMPTY_DETAIL = ""; - private DocumentEdits edits; - private String label; - private String detail; - private Renderable documentation; + private final DocumentEdits edits; + private final String label; + private final String detail; + private final Renderable documentation; + private final double score; - public QualifierCompletionProposal(DocumentEdits edits, String label, String detail, Renderable documentation) { + public QualifierCompletionProposal(DocumentEdits edits, String label, String detail, Renderable documentation, double score) { this.edits = edits; this.label = label; // PT 161489998 - Detail for proposal must not be null. For some clients like Eclipse, @@ -35,6 +36,7 @@ public class QualifierCompletionProposal implements ICompletionProposal { // in odd behaviour like insertion of an extra new line. this.detail = detail == null ? EMPTY_DETAIL : detail; this.documentation = documentation; + this.score = score; } @Override @@ -62,4 +64,9 @@ public class QualifierCompletionProposal implements ICompletionProposal { return this.documentation; } + @Override + public double getBaseScore() { + return this.score; + } + } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierCompletionProviderTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierCompletionProviderTest.java index 8a19c2ebe..513b0d419 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierCompletionProviderTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/beans/test/QualifierCompletionProviderTest.java @@ -95,7 +95,7 @@ public class QualifierCompletionProviderTest { @Test public void testQualifierCompletionWithoutQuotesWithoutPrefix() throws Exception { - assertCompletions("@Qualifier(<*>)", 4, new String[] {"bean1", "bean2", "quali1", "quali2"}, 0, "@Qualifier(\"bean1\"<*>)"); + assertCompletions("@Qualifier(<*>)", 4, new String[] {"quali1", "quali2", "bean1", "bean2"}, 0, "@Qualifier(\"quali1\"<*>)"); } // @Test