Bean completion invocation fixes

This commit is contained in:
aboyko
2025-02-25 22:50:04 -05:00
parent f78a41a481
commit 251221285e
16 changed files with 284 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2015, 2023 Pivotal, Inc.
* Copyright (c) 2015, 2025 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
@@ -12,11 +12,11 @@ package org.springframework.ide.vscode.boot.common;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal;
import org.springframework.ide.vscode.commons.languageserver.completion.AbstractScoreableProposal;
import org.springframework.ide.vscode.commons.util.text.IDocument;
import org.springframework.ide.vscode.commons.yaml.schema.YType;
public abstract class AbstractPropertyProposal extends ScoreableProposal {
public abstract class AbstractPropertyProposal extends AbstractScoreableProposal {
@Override
public String getDetail() {

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2014, 2023 Pivotal, Inc.
* Copyright (c) 2014, 2025 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
@@ -18,7 +18,7 @@ import org.springframework.ide.vscode.boot.metadata.types.TypeUtil;
import org.springframework.ide.vscode.boot.metadata.types.TypedProperty;
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.languageserver.completion.AbstractScoreableProposal;
import org.springframework.ide.vscode.commons.util.FuzzyMap.Match;
import org.springframework.ide.vscode.commons.util.Renderable;
import org.springframework.ide.vscode.commons.util.text.IDocument;
@@ -28,7 +28,7 @@ import org.springframework.ide.vscode.commons.yaml.schema.YType;
public class PropertyCompletionFactory {
public ICompletionProposal valueProposal(String value, String query, String niceTypeName, double score, DocumentEdits edits, Renderable info) {
return new ScoreableProposal() {
return new AbstractScoreableProposal() {
@Override
public DocumentEdits getTextEdit() {

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2024 Broadcom
* Copyright (c) 2024, 2025 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
@@ -12,13 +12,13 @@ package org.springframework.ide.vscode.boot.java.annotations;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal;
import org.springframework.ide.vscode.commons.languageserver.completion.AbstractScoreableProposal;
import org.springframework.ide.vscode.commons.util.Renderable;
/**
* @author Martin Lippert
*/
public class AnnotationAttributeCompletionProposal extends ScoreableProposal {
public class AnnotationAttributeCompletionProposal extends AbstractScoreableProposal {
private final AnnotationAttributeProposal coreProposal;

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017, 2024 Broadcom, Inc.
* Copyright (c) 2017, 2025 Broadcom, 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
@@ -18,7 +18,7 @@ import org.eclipse.lsp4j.Command;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.java.rewrite.RewriteRefactorings;
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.ICompletionProposalWithScore;
import org.springframework.ide.vscode.commons.rewrite.config.RecipeScope;
import org.springframework.ide.vscode.commons.rewrite.java.FixDescriptor;
import org.springframework.ide.vscode.commons.rewrite.java.InjectBeanCompletionRecipe;
@@ -30,7 +30,7 @@ import org.springframework.ide.vscode.commons.util.text.IDocument;
* @author Udayani V
* @author Alex Boyko
*/
public class BeanCompletionProposal implements ICompletionProposal {
public class BeanCompletionProposal implements ICompletionProposalWithScore {
private DocumentEdits edits;
private IDocument doc;
@@ -38,14 +38,17 @@ public class BeanCompletionProposal implements ICompletionProposal {
private String beanType;
private String className;
private RewriteRefactorings rewriteRefactorings;
private double score;
public BeanCompletionProposal(DocumentEdits edits, IDocument doc, String beanId, String beanType, String className,
double score,
RewriteRefactorings rewriteRefactorings) {
this.edits = edits;
this.doc = doc;
this.beanId = beanId;
this.beanType = beanType;
this.className = className;
this.score = score;
this.rewriteRefactorings = rewriteRefactorings;
}
@@ -82,6 +85,10 @@ public class BeanCompletionProposal implements ICompletionProposal {
.withRecipeScope(RecipeScope.NODE);
return Optional.of(rewriteRefactorings.createFixCommand("Inject bean '%s'".formatted(beanId), f));
}
@Override
public double getScore() {
return score;
}
}

View File

@@ -10,13 +10,19 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.java.beans;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
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.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IAnnotationBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclaration;
@@ -30,6 +36,7 @@ import org.springframework.ide.vscode.commons.languageserver.completion.Document
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.FuzzyMatcher;
import org.springframework.ide.vscode.commons.util.text.TextDocument;
/**
@@ -53,7 +60,7 @@ public class BeanCompletionProvider implements CompletionProvider {
@Override
public void provideCompletions(ASTNode node, int offset, TextDocument doc,
Collection<ICompletionProposal> completions) {
if (node instanceof SimpleName) {
if (node instanceof SimpleName || node instanceof Block) {
try {
// Don't look at anything inside Annotation or VariableDelcaration node
for (ASTNode n = node; n != null; n = n.getParent()) {
@@ -77,14 +84,36 @@ public class BeanCompletionProvider implements CompletionProvider {
if (isSpringComponent(topLevelClass)) {
String className = getFullyQualifiedName(topLevelClass);
Bean[] beans = this.springIndex.getBeansOfProject(project.getElementName());
ITypeBinding topLevelBeanType = topLevelClass.resolveBinding();
Set<String> declaredFiledsTypes = Arrays.stream(topLevelBeanType.getDeclaredFields())
.map(vd -> vd.getType())
.filter(Objects::nonNull)
.map(t -> t.getQualifiedName())
.collect(Collectors.toSet());
final String prefix = node instanceof Block ? "" : node.toString();
for (Bean bean : beans) {
DocumentEdits edits = new DocumentEdits(doc, false);
edits.replace(offset - node.toString().length(), offset, bean.getName());
// If current class is a bean - ignore it
if (className.equals(bean.getType())) {
continue;
}
// Filter out beans already injected into this class
if (declaredFiledsTypes.contains(bean.getType())) {
continue;
}
double score = FuzzyMatcher.matchScore(prefix, bean.getName());
if (score > 0) {
DocumentEdits edits = new DocumentEdits(doc, false);
if (node instanceof Block) {
edits.insert(offset, bean.getName());
} else {
edits.replace(offset - prefix.length(), offset, bean.getName());
}
BeanCompletionProposal proposal = new BeanCompletionProposal(edits, doc, bean.getName(),
bean.getType(), className, rewriteRefactorings);
BeanCompletionProposal proposal = new BeanCompletionProposal(edits, doc, bean.getName(),
bean.getType(), className, score, rewriteRefactorings);
completions.add(proposal);
completions.add(proposal);
}
}
}
} catch (Exception e) {
@@ -127,9 +156,10 @@ public class BeanCompletionProvider implements CompletionProvider {
}
private static String getFullyQualifiedName(TypeDeclaration typeDecl) {
if (typeDecl.resolveBinding() != null) {
String qualifiedName = typeDecl.resolveBinding().getQualifiedName();
return qualifiedName.replaceAll("\\.(?=[^\\.]+$)", "\\$");
ITypeBinding binding = typeDecl.resolveBinding();
if (binding != null) {
String qualifiedName = binding.getQualifiedName();
return qualifiedName/*.replaceAll("\\.(?=[^\\.]+$)", "\\$")*/;
}
CompilationUnit cu = (CompilationUnit) typeDecl.getRoot();
String packageName = cu.getPackage() != null ? cu.getPackage().getName().getFullyQualifiedName() : "";

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2017, 2020 Pivotal, Inc.
* Copyright (c) 2017, 2025 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,14 +14,14 @@ import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.boot.common.InformationTemplates;
import org.springframework.ide.vscode.boot.metadata.PropertyInfo;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal;
import org.springframework.ide.vscode.commons.languageserver.completion.AbstractScoreableProposal;
import org.springframework.ide.vscode.commons.util.FuzzyMap.Match;
import org.springframework.ide.vscode.commons.util.Renderable;
/**
* @author Martin Lippert
*/
public class ValuePropertyKeyProposal extends ScoreableProposal {
public class ValuePropertyKeyProposal extends AbstractScoreableProposal {
private static final String EMPTY_DETAIL = "";

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2019, 2024 Pivotal, Inc.
* Copyright (c) 2019, 2025 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
@@ -15,13 +15,13 @@ import java.util.function.Supplier;
import org.eclipse.lsp4j.CompletionItemKind;
import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits;
import org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal;
import org.springframework.ide.vscode.commons.languageserver.completion.AbstractScoreableProposal;
import org.springframework.ide.vscode.commons.util.Renderable;
/**
* @author Martin Lippert
*/
public class GenericXMLCompletionProposal extends ScoreableProposal {
public class GenericXMLCompletionProposal extends AbstractScoreableProposal {
private final String label;
private final CompletionItemKind kind;

View File

@@ -1,5 +1,5 @@
/*******************************************************************************
* Copyright (c) 2015, 2023 Pivotal, Inc.
* Copyright (c) 2015, 2025 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,7 +10,7 @@
*******************************************************************************/
package org.springframework.ide.vscode.boot.yaml.completions;
import static org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal.DEEMP_EXISTS;
import static org.springframework.ide.vscode.commons.languageserver.completion.AbstractScoreableProposal.DEEMP_EXISTS;
import java.util.ArrayList;
import java.util.Collection;
@@ -53,7 +53,7 @@ import org.springframework.ide.vscode.commons.java.IType;
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.LazyProposalApplier;
import org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal;
import org.springframework.ide.vscode.commons.languageserver.completion.AbstractScoreableProposal;
import org.springframework.ide.vscode.commons.util.CollectionUtil;
import org.springframework.ide.vscode.commons.util.FuzzyMap.Match;
import org.springframework.ide.vscode.commons.util.FuzzyMatcher;
@@ -466,7 +466,7 @@ public abstract class ApplicationYamlAssistContext extends AbstractYamlAssistCon
matchingProps.stream().forEach(match -> {
try {
DocumentEdits edits = createEdits(doc, node, offset, query, match);
ScoreableProposal completion = completionFactory.property(
AbstractScoreableProposal completion = completionFactory.property(
doc.getDocument(), edits, match, typeUtil
);
String prefix = indexNav.getPrefix();

View File

@@ -65,6 +65,7 @@ public class BeanCompletionProviderTest {
private Bean bean3;
private Bean bean4;
private Bean bean5;
private Bean bean6;
@BeforeEach
public void setup() throws Exception {
@@ -86,8 +87,9 @@ public class BeanCompletionProviderTest {
bean3 = new Bean("visitRepository", "org.springframework.samples.petclinic.owner.VisitRepository", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false, "symbolLabel");
bean4 = new Bean("visitService", "org.springframework.samples.petclinic.owner.VisitService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false, "symbolLabel");
bean5 = new Bean("petService", "org.springframework.samples.petclinic.pet.Inner.PetService", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false, "symbolLabel");
bean6 = new Bean("testBeanCompletionClass", "org.sample.test.TestBeanCompletionClass", new Location(tempJavaDocUri, new Range(new Position(1,1), new Position(1, 20))), null, null, null, false, "symbolLabel");
springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3, bean4, bean5});
springIndex.updateBeans(project.getElementName(), new Bean[] {bean1, bean2, bean3, bean4, bean5, bean6});
}
@AfterEach
@@ -97,7 +99,7 @@ public class BeanCompletionProviderTest {
@Test
public void testBeanCompletion_firstCompletion() throws Exception {
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 0,
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService"}, 0,
"""
package org.sample.test;
@@ -122,7 +124,32 @@ ownerRepository<*>
@Test
public void testBeanCompletion_secondCompletion() throws Exception {
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 1,
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService"}, 1,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerService;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerService ownerService;
TestBeanCompletionClass(OwnerService ownerService) {
this.ownerService = ownerService;
}
public void test() {
ownerService<*>
}
}
""");
}
@Test
public void noPrefix_secondCompletion() throws Exception {
assertCompletions(getCompletion("<*>"), new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 1,
"""
package org.sample.test;
@@ -147,7 +174,7 @@ ownerService<*>
@Test
public void testBeanCompletion_injectInnerClass() throws Exception {
assertCompletions(getCompletion("owner<*>"), new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 2,
assertCompletions(getCompletion("<*>"), new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 2,
"""
package org.sample.test;
@@ -199,7 +226,72 @@ petService<*>
}
""";
assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 1,
assertCompletions(content, new String[] {"ownerRepository", "ownerService"}, 1,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.samples.petclinic.owner.OwnerService;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerRepository ownerRepository;
TestBeanCompletionClass(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
@Controller
public class TestBeanCompletionSecondClass {
private final OwnerService ownerService;
TestBeanCompletionSecondClass(OwnerService ownerService) {
this.ownerService = ownerService;
}
public void test() {
ownerService<*>
}
}
""");
}
@Test
public void noPrefix_multipleClasses() throws Exception {
String content = """
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionClass {
private final OwnerRepository ownerRepository;
TestBeanCompletionClass(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
public void test() {
}
}
@Controller
public class TestBeanCompletionSecondClass {
public void test() {
<*>
}
}
""";
assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "testBeanCompletionClass", "visitRepository", "visitService"}, 1,
"""
package org.sample.test;
@@ -279,13 +371,13 @@ public class TestBeanCompletionClass {
public class Inner {
public void test() {
ownerRe<*>
owner<*>
}
}
}
""";
assertCompletions(content, new String[] {"ownerRepository", "ownerService", "petService", "visitRepository", "visitService"}, 0,
assertCompletions(content, new String[] {"ownerRepository", "ownerService"}, 0,
"""
package org.sample.test;
@@ -330,6 +422,68 @@ public class TestBeanCompletionClass {
return content;
}
@Test
public void beansPresent() throws Exception {
String content = """
package org.sample.test;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionSecondClass {
private final TestBeanCompletionClass testBeanCompletionClass;
TestBeanCompletionSecondClass(TestBeanCompletionClass testBeanCompletionClass) {
this.testBeanCompletionClass = testBeanCompletionClass;
}
public void test() {
owner<*>
}
}
@Controller
public class TestBeanCompletionClass {
public void test() {
}
}
""";
assertCompletions(content, new String[] {"ownerRepository", "ownerService"}, 0,
"""
package org.sample.test;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.stereotype.Controller;
@Controller
public class TestBeanCompletionSecondClass {
private final OwnerRepository ownerRepository;
private final TestBeanCompletionClass testBeanCompletionClass;
TestBeanCompletionSecondClass(TestBeanCompletionClass testBeanCompletionClass, OwnerRepository ownerRepository) {
this.testBeanCompletionClass = testBeanCompletionClass;
this.ownerRepository = ownerRepository;
}
public void test() {
ownerRepository<*>
}
}
@Controller
public class TestBeanCompletionClass {
public void test() {
}
}
""");
}
private void assertCompletions(String completionLine, String[] expectedCompletions, int chosenCompletion, String expectedResult) throws Exception {
assertCompletions(completionLine, expectedCompletions.length, expectedCompletions, chosenCompletion, expectedResult);
}