From e273a94db7f185d5e439ae41d3d1418629c6e8e6 Mon Sep 17 00:00:00 2001 From: aboyko Date: Thu, 20 Apr 2023 18:39:33 -0400 Subject: [PATCH] Support type import for spring data repository completions --- .../languageserver/testharness/Editor.java | 14 ++- .../DataRepositoryCompletionProcessor.java | 24 ++--- .../java/data/DataRepositoryDefinition.java | 11 ++- .../ide/vscode/boot/java/data/DomainType.java | 55 +++++------ .../java/data/FindByCompletionProposal.java | 7 +- .../ide/vscode/boot/java/data/SimpleType.java | 55 +++++++++++ .../DataRepositoryCompletionProvider.java | 3 +- ...epositoryQueryStartCompletionProvider.java | 6 +- ...aRepositoryStandardCompletionProvider.java | 27 ++++- .../data/providers/QueryMethodSubject.java | 25 ++--- ...toryPrefixSensitiveCompletionProvider.java | 32 ++++-- .../ide/vscode/boot/java/utils/ASTUtils.java | 18 +++- ...DataRepositoryCompletionProcessorTest.java | 98 ++++++++++++++++++- .../test/RequestMappingSnippetTests.java | 4 +- .../src/main/java/org/test/Application.java | 1 + .../src/main/java/org/test/Customer.java | 3 + .../java/org/test/{ => model}/Employee.java | 2 +- .../java/org/test/{ => model}/Person.java | 2 +- 18 files changed, 304 insertions(+), 83 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/SimpleType.java rename headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/{ => model}/Employee.java (96%) rename headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/{ => model}/Person.java (93%) diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 47ac46c21..8db4cbedc 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2020 Pivotal, Inc. + * Copyright (c) 2016, 2023 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 @@ -494,6 +494,18 @@ public class Editor { selectionEnd += insertText.length(); setRawText(newText); } + + // Experimental additional edit support. Only makes good sense when it is above the text being edited + if (completion.getAdditionalTextEdits() != null) { + for (TextEdit te : completion.getAdditionalTextEdits()) { + String replaceWith = te.getNewText(); + Range rng = te.getRange(); + int start = doc.toOffset(rng.getStart()); + int end = doc.toOffset(rng.getEnd()); + replaceText(start, end, replaceWith); + selectionStart = selectionEnd = selectionStart - (end - start) + replaceWith.length(); + } + } } private String getInsertText(CompletionItem completion) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java index 387656106..cff1af18f 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryCompletionProcessor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2018, 2023 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 @@ -61,7 +61,7 @@ public class DataRepositoryCompletionProcessor implements CompletionProvider { // ignore if there is a problem computing the prefix, continue without prefix } for(DataRepositoryCompletionProvider provider : completionProviders){ - provider.addProposals(completions, doc, offset, prefix, repo); + provider.addProposals(completions, doc, offset, prefix, repo, node); } } } @@ -90,14 +90,7 @@ public class DataRepositoryCompletionProcessor implements CompletionProvider { } if (Constants.REPOSITORY_TYPE.equals(simplifiedType)) { - DomainType domainType = null; - if (resolvedInterface.isParameterizedType()) { - ITypeBinding[] typeParameters = resolvedInterface.getTypeArguments(); - if (typeParameters != null && typeParameters.length > 0) { - domainType = new DomainType(typeParameters[0]); - } - } - return createDataRepositoryDefinitionFromType(domainType); + return createDataRepositoryDefinitionFromType(resolvedInterface); } else { DataRepositoryDefinition repo = getDataRepositoryDefinition(type, resolvedInterface); @@ -116,7 +109,14 @@ public class DataRepositoryCompletionProcessor implements CompletionProvider { return null; } - private DataRepositoryDefinition createDataRepositoryDefinitionFromType(DomainType domainType) { - return new DataRepositoryDefinition(domainType); + private DataRepositoryDefinition createDataRepositoryDefinitionFromType(ITypeBinding resolvedInterface) { + DomainType domainType = null; + if (resolvedInterface.isParameterizedType()) { + ITypeBinding[] typeParameters = resolvedInterface.getTypeArguments(); + if (typeParameters != null && typeParameters.length > 0) { + domainType = new DomainType(typeParameters[0]); + } + } + return new DataRepositoryDefinition(new SimpleType(resolvedInterface),domainType); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryDefinition.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryDefinition.java index 25153453c..e2f3fe6ee 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryDefinition.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DataRepositoryDefinition.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018 Pivotal, Inc. + * Copyright (c) 2018, 2023 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,15 +15,20 @@ package org.springframework.ide.vscode.boot.java.data; */ public class DataRepositoryDefinition { + private final SimpleType type; private final DomainType domainType; - public DataRepositoryDefinition(DomainType domainType) { - super(); + public DataRepositoryDefinition(SimpleType type, DomainType domainType) { + this.type = type; this.domainType = domainType; } public DomainType getDomainType() { return domainType; } + + public SimpleType getType() { + return type; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java index c06841832..1fd2f6998 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/DomainType.java @@ -12,9 +12,11 @@ package org.springframework.ide.vscode.boot.java.data; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; @@ -25,36 +27,37 @@ import com.google.common.base.Suppliers; /** * @author Martin Lippert */ -public class DomainType { +public class DomainType extends SimpleType { - private final String packageName; - private final String fullName; - private final String simpleName; private Supplier properties; - - public DomainType(String packageName, String fullName, String simpleName) { - this.packageName = packageName; - this.fullName = fullName; - this.simpleName = simpleName; - } + private Set usedTypes; public DomainType(ITypeBinding typeBinding) { - if (typeBinding.getPackage() == null) { - this.packageName = ""; - } else { - this.packageName = typeBinding.getPackage().getName(); - } - this.fullName = typeBinding.getQualifiedName(); - this.simpleName = typeBinding.getName(); + super(typeBinding); this.properties = Suppliers.memoize(() -> { List domainProps = calculateDomainProperties(typeBinding); return domainProps.toArray(new DomainProperty[domainProps.size()]); }); + + this.usedTypes = new HashSet<>(); + fillImports(typeBinding); + } + + private void fillImports(ITypeBinding binding) { + if (binding != binding.getErasure()) { + fillImports(binding.getErasure()); + for (ITypeBinding ta : binding.getTypeArguments()) { + fillImports(ta); + } + } else if (binding.isClass() || binding.isInterface() || binding.isEnum() || binding.isRecord()) { + String fqName = binding.getQualifiedName(); + usedTypes.add(fqName); + } } private List calculateDomainProperties(ITypeBinding typeBinding) { - if (!this.packageName.startsWith("java")) { + if (!getPackageName().startsWith("java")) { IMethodBinding[] methods = typeBinding.getDeclaredMethods(); if (methods != null && methods.length > 0) { List properties = new ArrayList<>(); @@ -91,18 +94,6 @@ public class DomainType { return Collections.emptyList(); } - public String getPackageName() { - return packageName; - } - - public String getFullName() { - return fullName; - } - - public String getSimpleName() { - return simpleName; - } - public DomainProperty[] getProperties() { return properties.get(); } @@ -114,5 +105,9 @@ public class DomainType { } return propertiesByName; } + + public Set getUsedTypes() { + return usedTypes; + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/FindByCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/FindByCompletionProposal.java index 0b4316652..b3de40c50 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/FindByCompletionProposal.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/FindByCompletionProposal.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2018, 2019 Pivotal, Inc. + * Copyright (c) 2018, 2023 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 @@ -41,7 +41,7 @@ public class FindByCompletionProposal implements ICompletionProposal { this.triggerNextCompletion = triggerNextCompletion; } - public static ICompletionProposal createProposal(int offset, CompletionItemKind completionItemKind, String prefix, String label, String completion, boolean triggerNextCompletion) { + public static ICompletionProposal createProposal(int offset, CompletionItemKind completionItemKind, String prefix, String label, String completion, boolean triggerNextCompletion, Optional additionalEdits) { DocumentEdits edits = new DocumentEdits(null, false); String filter = label; if (prefix != null && label.startsWith(prefix)) { @@ -55,8 +55,7 @@ public class FindByCompletionProposal implements ICompletionProposal { edits.insert(offset, completion); } - DocumentEdits additionalEdits = new DocumentEdits(null, false); - return new FindByCompletionProposal(label, completionItemKind, edits, null, null, Optional.of(additionalEdits), filter, triggerNextCompletion); + return new FindByCompletionProposal(label, completionItemKind, edits, null, null, additionalEdits, filter, triggerNextCompletion); } @Override diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/SimpleType.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/SimpleType.java new file mode 100644 index 000000000..878b24dd7 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/SimpleType.java @@ -0,0 +1,55 @@ +/******************************************************************************* + * Copyright (c) 2023 VMware, 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 + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * VMware, Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.java.data; + +import org.eclipse.jdt.core.dom.ITypeBinding; + +public class SimpleType { + + private final String packageName; + private final String fullName; + private final String simpleName; + + public SimpleType(ITypeBinding typeBinding) { + if (typeBinding.getPackage() == null) { + this.packageName = ""; + } else { + this.packageName = typeBinding.getPackage().getName(); + } + this.fullName = typeBinding.getQualifiedName(); + this.simpleName = typeBinding.getName(); + } + + public String getPackageName() { + return packageName; + } + + public String getFullName() { + return fullName; + } + + public String getSimpleName() { + return simpleName; + } + + public boolean shouldImportType(String fqName) { + if (fqName.startsWith("java.lang.")) { + return false; + } + if (fqName.startsWith(packageName) && fqName.length() >= packageName.length() + 2 // +2: one for '.', second for capital letter + && fqName.charAt(packageName.length()) == '.' && Character.isUpperCase(fqName.charAt(packageName.length() + 1))) { + return false; + } + return true; + + } + +} diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryCompletionProvider.java index 9a0af9feb..e651bd283 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryCompletionProvider.java @@ -12,6 +12,7 @@ package org.springframework.ide.vscode.boot.java.data.providers; import java.util.Collection; +import org.eclipse.jdt.core.dom.ASTNode; import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.util.text.IDocument; @@ -22,6 +23,6 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; */ public interface DataRepositoryCompletionProvider { - void addProposals(Collection completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo); + void addProposals(Collection completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo, ASTNode node); } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryQueryStartCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryQueryStartCompletionProvider.java index e3c68809e..e409916eb 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryQueryStartCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryQueryStartCompletionProvider.java @@ -11,7 +11,9 @@ package org.springframework.ide.vscode.boot.java.data.providers; import java.util.Collection; +import java.util.Optional; +import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.lsp4j.CompletionItemKind; import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition; import org.springframework.ide.vscode.boot.java.data.FindByCompletionProposal; @@ -27,12 +29,12 @@ import org.springframework.ide.vscode.commons.util.text.IDocument; public class DataRepositoryQueryStartCompletionProvider implements DataRepositoryCompletionProvider{ @Override - public void addProposals(Collection completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo) { + public void addProposals(Collection completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo, ASTNode node) { String localPrefix = DataRepositoryPrefixSensitiveCompletionProvider.findLastJavaIdentifierPart(prefix); for(QueryMethodSubject queryMethodSubject : QueryMethodSubject.QUERY_METHOD_SUBJECTS){ String toInsert = queryMethodSubject.key() + "By"; if(prefix == null || (toInsert.length() > localPrefix.length() && toInsert.startsWith(localPrefix)) || isOffsetAfterWhitespace(doc, offset)) { - completions.add(FindByCompletionProposal.createProposal(offset, CompletionItemKind.Text, prefix, toInsert, toInsert, true)); + completions.add(FindByCompletionProposal.createProposal(offset, CompletionItemKind.Text, prefix, toInsert, toInsert, true, Optional.empty())); } } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryStandardCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryStandardCompletionProvider.java index 3a21245b2..d1546eddd 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryStandardCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/DataRepositoryStandardCompletionProvider.java @@ -11,17 +11,26 @@ package org.springframework.ide.vscode.boot.java.data.providers; import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.lsp4j.CompletionItemKind; import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition; import org.springframework.ide.vscode.boot.java.data.DomainProperty; import org.springframework.ide.vscode.boot.java.data.DomainType; import org.springframework.ide.vscode.boot.java.data.FindByCompletionProposal; import org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive.DataRepositoryPrefixSensitiveCompletionProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.util.StringUtils; +import com.google.common.base.Objects; + /** * Provides content assist proposals for querying by a single attribute in Spring Data repositories. * @author Martin Lippert @@ -30,16 +39,17 @@ public class DataRepositoryStandardCompletionProvider implements DataRepositoryC private static final String FIND_BY = "findBy"; - public void addProposals(Collection completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo) { + public void addProposals(Collection completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repo, ASTNode node) { if (FIND_BY.startsWith(DataRepositoryPrefixSensitiveCompletionProvider.findLastJavaIdentifierPart(prefix))) { DomainType domainType = repo.getDomainType(); for (DomainProperty property : domainType.getPropertiesByName().values()) { - completions.add(generateCompletionProposal(offset, prefix, repo, property)); + completions.add(generateCompletionProposal(offset, prefix, repo, property, doc, node)); } } } - private ICompletionProposal generateCompletionProposal(int offset, String prefix, DataRepositoryDefinition repoDef, DomainProperty domainProperty) { + private ICompletionProposal generateCompletionProposal(int offset, String prefix, DataRepositoryDefinition repoDef, DomainProperty domainProperty, IDocument doc, ASTNode node) { + Set imprts = new HashSet<>(); StringBuilder label = new StringBuilder(); label.append("findBy"); label.append(StringUtils.capitalize(domainProperty.getName())); @@ -50,16 +60,23 @@ public class DataRepositoryStandardCompletionProvider implements DataRepositoryC label.append(");"); StringBuilder completion = new StringBuilder(); + imprts.add(List.class.getName()); completion.append("List<"); completion.append(repoDef.getDomainType().getSimpleName()); completion.append("> findBy"); completion.append(StringUtils.capitalize(domainProperty.getName())); completion.append("("); completion.append(domainProperty.getType().getSimpleName()); + if (!Objects.equal(repoDef.getDomainType().getPackageName(), domainProperty.getType().getPackageName())) { + imprts.addAll(domainProperty.getType().getUsedTypes().stream() + .filter(repoDef.getType()::shouldImportType) + .collect(Collectors.toList())); + } completion.append(" "); completion.append(StringUtils.uncapitalize(domainProperty.getName())); completion.append(");"); - - return FindByCompletionProposal.createProposal(offset, CompletionItemKind.Method, prefix, label.toString(), completion.toString(), false); + + return FindByCompletionProposal.createProposal(offset, CompletionItemKind.Method, prefix, label.toString(), completion.toString(), false, ASTUtils.getAdditionalEdit((CompilationUnit)node.getRoot(), imprts, doc)); } + } \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/QueryMethodSubject.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/QueryMethodSubject.java index 445065480..e0820149d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/QueryMethodSubject.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/QueryMethodSubject.java @@ -19,15 +19,18 @@ import java.util.List; * @author danthe1st */ public record QueryMethodSubject( - String key, String returnType, boolean isTyped) { - + String key, String returnType, String fqName) { + + private static final String LIST = "List"; + private static final String LIST_FQ = "java.util.List"; + public static final List QUERY_METHOD_SUBJECTS = List.of( - QueryMethodSubject.createCollectionSubject("find", "List"), - QueryMethodSubject.createCollectionSubject("read", "List"), - QueryMethodSubject.createCollectionSubject("get", "List"), - QueryMethodSubject.createCollectionSubject("query", "List"), - QueryMethodSubject.createCollectionSubject("search", "List"), - QueryMethodSubject.createCollectionSubject("stream", "Streamable"), + QueryMethodSubject.createCollectionSubject("find", LIST, LIST_FQ), + QueryMethodSubject.createCollectionSubject("read", LIST, LIST_FQ), + QueryMethodSubject.createCollectionSubject("get", LIST, LIST_FQ), + QueryMethodSubject.createCollectionSubject("query", LIST, LIST_FQ), + QueryMethodSubject.createCollectionSubject("search", LIST, LIST_FQ), + QueryMethodSubject.createCollectionSubject("stream", "Streamable", "org.springframework.data.util.Streamable"), QueryMethodSubject.createPrimitiveSubject("exists", "boolean"), QueryMethodSubject.createPrimitiveSubject("count", "long"), QueryMethodSubject.createPrimitiveSubject("delete", "void"), @@ -35,10 +38,10 @@ public record QueryMethodSubject( ); private static QueryMethodSubject createPrimitiveSubject(String key, String primitive) { - return new QueryMethodSubject(key, primitive, false); + return new QueryMethodSubject(key, primitive, null); } - private static QueryMethodSubject createCollectionSubject(String key, String collectionType) { - return new QueryMethodSubject(key, collectionType, true); + private static QueryMethodSubject createCollectionSubject(String key, String collectionTypeSimpleName, String collectionTypeFqName) { + return new QueryMethodSubject(key, collectionTypeSimpleName, collectionTypeFqName); } } \ No newline at end of file diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryPrefixSensitiveCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryPrefixSensitiveCompletionProvider.java index 8ecb84f8a..9cdde8979 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryPrefixSensitiveCompletionProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/data/providers/prefixsensitive/DataRepositoryPrefixSensitiveCompletionProvider.java @@ -11,16 +11,21 @@ package org.springframework.ide.vscode.boot.java.data.providers.prefixsensitive; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.lsp4j.CompletionItemKind; import org.springframework.ide.vscode.boot.java.data.DataRepositoryDefinition; import org.springframework.ide.vscode.boot.java.data.DomainProperty; import org.springframework.ide.vscode.boot.java.data.DomainType; import org.springframework.ide.vscode.boot.java.data.FindByCompletionProposal; import org.springframework.ide.vscode.boot.java.data.providers.DataRepositoryCompletionProvider; +import org.springframework.ide.vscode.boot.java.utils.ASTUtils; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; import org.springframework.ide.vscode.commons.util.text.IDocument; @@ -33,7 +38,7 @@ import org.springframework.util.StringUtils; public class DataRepositoryPrefixSensitiveCompletionProvider implements DataRepositoryCompletionProvider { @Override - public void addProposals(Collection completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repoDef) { + public void addProposals(Collection completions, IDocument doc, int offset, String prefix, DataRepositoryDefinition repoDef, ASTNode node) { String localPrefix = findLastJavaIdentifierPart(prefix); if (localPrefix == null) { return; @@ -42,7 +47,7 @@ public class DataRepositoryPrefixSensitiveCompletionProvider implements DataRepo if(parseResult != null && parseResult.performFullCompletion()){ Map propertiesByName = repoDef.getDomainType().getPropertiesByName(); if (isEndingWithProperty(parseResult, propertiesByName) || isEndingWithPredicateKeyWord(localPrefix, propertiesByName)) { - addMethodCompletionProposal(completions, offset, repoDef, localPrefix, prefix, parseResult, propertiesByName); + addMethodCompletionProposal(completions, offset, repoDef, localPrefix, prefix, parseResult, propertiesByName, node, doc); } if (parseResult.lastWord() == null || !propertiesByName.containsKey(parseResult.lastWord())) { @@ -115,13 +120,17 @@ public class DataRepositoryPrefixSensitiveCompletionProvider implements DataRepo } } - private void addMethodCompletionProposal(Collection completions, int offset, DataRepositoryDefinition repoDef, String localPrefix, String fullPrefix, DataRepositoryMethodNameParseResult parseResult, Map propertiesByName) { + private void addMethodCompletionProposal(Collection completions, int offset, DataRepositoryDefinition repoDef, String localPrefix, String fullPrefix, DataRepositoryMethodNameParseResult parseResult, Map propertiesByName, ASTNode node, IDocument doc) { String methodName = localPrefix; DocumentEdits edits = new DocumentEdits(null, false); - String signature = buildSignature(methodName, propertiesByName, parseResult); + Set imports = new HashSet<>(); + String signature = buildSignature(methodName, propertiesByName, parseResult, imports, repoDef); StringBuilder newText = new StringBuilder(); newText.append(parseResult.subjectType().returnType()); - if (parseResult.subjectType().isTyped()) { + if (parseResult.subjectType().fqName() != null && repoDef.getType().shouldImportType(parseResult.subjectType().fqName())) { + imports.add(parseResult.subjectType().fqName()); + } + if (parseResult.subjectType().fqName() != null) { newText.append("<"); newText.append(repoDef.getDomainType().getSimpleName()); newText.append(">"); @@ -132,8 +141,8 @@ public class DataRepositoryPrefixSensitiveCompletionProvider implements DataRepo newText.append(";"); int replaceStart = calculateReplaceOffset(offset, localPrefix, fullPrefix, returnType); edits.replace(replaceStart, offset, newText.toString()); - DocumentEdits additionalEdits = new DocumentEdits(null, false); - ICompletionProposal proposal = new FindByCompletionProposal(signature, CompletionItemKind.Method, edits, null, null, Optional.of(additionalEdits), signature, false); + Optional additionalEdits = ASTUtils.getAdditionalEdit((CompilationUnit) node.getRoot(), imports, doc); + ICompletionProposal proposal = new FindByCompletionProposal(signature, CompletionItemKind.Method, edits, null, null, additionalEdits, signature, false); completions.add(proposal); } @@ -147,7 +156,7 @@ public class DataRepositoryPrefixSensitiveCompletionProvider implements DataRepo return replaceStart; } - private String buildSignature(String methodName, Map properties, DataRepositoryMethodNameParseResult parseResult) { + private String buildSignature(String methodName, Map properties, DataRepositoryMethodNameParseResult parseResult, Set imports, DataRepositoryDefinition repoDef) { StringBuilder signatureBuilder = new StringBuilder(); signatureBuilder.append(methodName); signatureBuilder.append("("); @@ -157,8 +166,13 @@ public class DataRepositoryPrefixSensitiveCompletionProvider implements DataRepo DomainType type = findExpressionType(properties, param); if (type == null) { signatureBuilder.append("Object"); - }else { + } else { signatureBuilder.append(type.getSimpleName()); + for (String im : type.getUsedTypes()) { + if (repoDef.getType().shouldImportType(im)) { + imports.add(im); + } + } } signatureBuilder.append(" "); signatureBuilder.append(StringUtils.uncapitalize(param)); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java index 2e792c8a7..495f86140 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/utils/ASTUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2017, 2019 Pivotal, Inc. + * Copyright (c) 2017, 2023 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 @@ -20,6 +20,7 @@ import java.util.stream.Stream; 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.CompilationUnit; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.IBinding; import org.eclipse.jdt.core.dom.ITypeBinding; @@ -37,8 +38,11 @@ import org.eclipse.lsp4j.Range; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ide.vscode.boot.java.Annotations; +import org.springframework.ide.vscode.boot.java.jdt.imports.ImportRewrite; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; import org.springframework.ide.vscode.commons.util.CollectorUtil; import org.springframework.ide.vscode.commons.util.text.DocumentRegion; +import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; @@ -302,5 +306,17 @@ public class ASTUtils { } return null; } + + public static Optional getAdditionalEdit(CompilationUnit cu, Collection imprts, IDocument doc) { + ImportRewrite rewrite = ImportRewrite.create(cu, true); + + for (String imprt : imprts) { + rewrite.addImport(imprt); + } + + DocumentEdits edit = rewrite.createEdit(doc); + + return edit != null ? Optional.of(edit) : Optional.empty(); + } } diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java index dc4fab12d..f82a591e5 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/data/test/DataRepositoryCompletionProcessorTest.java @@ -17,6 +17,8 @@ import java.util.List; import org.apache.commons.io.IOUtils; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + import org.eclipse.lsp4j.CompletionItem; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,7 +42,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; @BootLanguageServerTest @Import(HoverTestConf.class) public class DataRepositoryCompletionProcessorTest { - + @Autowired private BootLanguageServerHarness harness; private Editor editor; @@ -80,7 +82,85 @@ public class DataRepositoryCompletionProcessorTest { "searchBy", "streamBy"); } + + @Test + void assertCompletionWithImportDifferentAndSamePackage() throws Exception { + checkCompletionResult("", "findByResponsibleEmployee", """ + package org.test; + import org.springframework.data.repository.CrudRepository; + import java.util.List; + import org.test.model.Employee; + + + public interface TestCustomerRepositoryForCompletions extends CrudRepository { + List findByResponsibleEmployee(Employee responsibleEmployee);<*> + } + """); + } + + @Test + void assertCompletionWithImportFromJavaLang() throws Exception { + checkCompletionResult("", "findById", """ + package org.test; + + import org.springframework.data.repository.CrudRepository; + import java.util.List; + + + public interface TestCustomerRepositoryForCompletions extends CrudRepository { + List findById(Long id);<*> + } + """); + } + + @Test + void prefixSensitiveMethodCompletionWithImports_1() throws Exception { + checkCompletionResult("findByResponsibleEmployeeAndLastName", "findByResponsibleEmployeeAndLastName", """ + package org.test; + + import org.springframework.data.repository.CrudRepository; + import org.test.model.Employee; + import java.util.List; + + + public interface TestCustomerRepositoryForCompletions extends CrudRepository { + List findByResponsibleEmployeeAndLastName(Employee responsibleEmployee, String lastName);<*> + } + """); + } + + @Test + void prefixSensitiveMethodCompletionWithImports_2() throws Exception { + checkCompletionResult("streamByResponsibleEmployeeAndLastName", "streamByResponsibleEmployeeAndLastName", """ + package org.test; + + import org.springframework.data.repository.CrudRepository; + import org.test.model.Employee; + import org.springframework.data.util.Streamable; + + + public interface TestCustomerRepositoryForCompletions extends CrudRepository { + Streamable streamByResponsibleEmployeeAndLastName(Employee responsibleEmployee, String lastName);<*> + } + """); + } + + @Test + void prefixSensitiveMethodCompletionWithImports_3() throws Exception { + checkCompletionResult("countByResponsibleEmployee", "countByResponsibleEmployee", """ + package org.test; + + import org.springframework.data.repository.CrudRepository; + import org.test.model.Employee; + + + public interface TestCustomerRepositoryForCompletions extends CrudRepository { + long countByResponsibleEmployee(Employee responsibleEmployee);<*> + } + """); + } + @Test void testPrefixSensitiveCompletionsCompleteMethod() throws Exception { checkCompletions("findByFirstNameAndLastName", "List findByFirstNameAndLastName(String firstName, String lastName);"); @@ -166,6 +246,22 @@ public class DataRepositoryCompletionProcessorTest { "findByFirstNameAfter", "findByFirstNameAnd"); } + + private void checkCompletionResult(String prefix, String completionLabel, String result) throws Exception { + prepareCase("{\n}", "{\n\t" + prefix + "<*>\n}"); + List completions = editor.getCompletions(); + + int i = 0; + for (CompletionItem foundCompletion : completions) { + if (foundCompletion.getLabel().contains(completionLabel)) { + Editor clonedEditor = editor.clone(); + clonedEditor.apply(foundCompletion); + assertEquals(result, clonedEditor.getText()); + return; + } + } + fail("Didn't find the proposal with label: " + completionLabel); + } private void checkCompletions(String alredyPresent, String... expectedCompletions) throws Exception { prepareCase("{\n}", "{\n\t" + alredyPresent + "<*>"); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSnippetTests.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSnippetTests.java index 97f75616f..96d26fd62 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSnippetTests.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/requestmapping/test/RequestMappingSnippetTests.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 Pivotal, Inc. + * Copyright (c) 2022, 2023 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 @@ -60,6 +60,8 @@ public class RequestMappingSnippetTests { + "import org.springframework.web.bind.annotation.RequestBody;\n" + "import org.springframework.web.bind.annotation.RequestMapping;\n" + "import org.springframework.web.bind.annotation.ResponseBody;\n" + + "import org.springframework.web.bind.annotation.RequestParam;\n" + + "\n" + "\n" + "/** Boot Java - Test Completion */\n" + "@Controller\n" diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Application.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Application.java index a1c94d869..7c96804f9 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Application.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Application.java @@ -6,6 +6,7 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; +import org.test.model.Employee; @SpringBootApplication public class Application { diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Customer.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Customer.java index 9a675ac7c..f59391907 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Customer.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Customer.java @@ -7,6 +7,9 @@ import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.ManyToOne; +import org.test.model.Employee; +import org.test.model.Person; + @Entity public class Customer extends Person { diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Employee.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/model/Employee.java similarity index 96% rename from headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Employee.java rename to headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/model/Employee.java index 46376d384..1f8f44b07 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Employee.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/model/Employee.java @@ -1,5 +1,5 @@ // tag::sample[] -package org.test; +package org.test.model; import javax.persistence.Entity; import javax.persistence.Id; diff --git a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Person.java b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/model/Person.java similarity index 93% rename from headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Person.java rename to headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/model/Person.java index 9778c4a78..2cf19cbec 100644 --- a/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/Person.java +++ b/headless-services/spring-boot-language-server/src/test/resources/test-projects/test-spring-data-symbols/src/main/java/org/test/model/Person.java @@ -1,4 +1,4 @@ -package org.test; +package org.test.model; public class Person {