From 801e5d233dff22627a8e2655288acffb9bd939c0 Mon Sep 17 00:00:00 2001 From: Erik Paulson Date: Wed, 27 Oct 2021 23:27:11 -0700 Subject: [PATCH] Auto-import completed classes This patch uses the CompilationUnitTree of the file being processed for a completion to build a Set of imports for the current file and to get the path to the file. It then uses the list of imports to determine whether the completion candidate has already been imported. If not, the AddImport class is used to add to the additionalTextEdits field on the CompletionItem. Some small refactoring was done to pull useful class and package name extractors out of JavaCompilerService.java. --- src/main/java/org/javacs/Extractors.java | 26 +++++++++++++ .../java/org/javacs/JavaCompilerService.java | 30 +++----------- .../javacs/completion/CompletionProvider.java | 39 ++++++++++++++++++- 3 files changed, 68 insertions(+), 27 deletions(-) create mode 100644 src/main/java/org/javacs/Extractors.java diff --git a/src/main/java/org/javacs/Extractors.java b/src/main/java/org/javacs/Extractors.java new file mode 100644 index 00000000..5e1a40c1 --- /dev/null +++ b/src/main/java/org/javacs/Extractors.java @@ -0,0 +1,26 @@ +package org.javacs; + +import java.util.regex.Pattern; + +public class Extractors { + + private static final Pattern PACKAGE_EXTRACTOR = Pattern.compile("^([a-z][_a-zA-Z0-9]*\\.)*[a-z][_a-zA-Z0-9]*"); + + public static String packageName(String className) { + var m = PACKAGE_EXTRACTOR.matcher(className); + if (m.find()) { + return m.group(); + } + return ""; + } + + private static final Pattern SIMPLE_EXTRACTOR = Pattern.compile("[A-Z][_a-zA-Z0-9]*$"); + + public static String simpleName(String className) { + var m = SIMPLE_EXTRACTOR.matcher(className); + if (m.find()) { + return m.group(); + } + return ""; + } +} diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java index cfe72783..6e7e1c7c 100644 --- a/src/main/java/org/javacs/JavaCompilerService.java +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -97,26 +97,6 @@ private CompileBatch compileBatch(Collection sources) return cachedCompile; } - private static final Pattern PACKAGE_EXTRACTOR = Pattern.compile("^([a-z][_a-zA-Z0-9]*\\.)*[a-z][_a-zA-Z0-9]*"); - - private String packageName(String className) { - var m = PACKAGE_EXTRACTOR.matcher(className); - if (m.find()) { - return m.group(); - } - return ""; - } - - private static final Pattern SIMPLE_EXTRACTOR = Pattern.compile("[A-Z][_a-zA-Z0-9]*$"); - - private String simpleName(String className) { - var m = SIMPLE_EXTRACTOR.matcher(className); - if (m.find()) { - return m.group(); - } - return ""; - } - private static final Cache cacheContainsWord = new Cache<>(); private boolean containsWord(Path file, String word) { @@ -206,7 +186,7 @@ public List packagePrivateTopLevelTypes(String packageName) { } private boolean containsImport(Path file, String className) { - var packageName = packageName(className); + var packageName = Extractors.packageName(className); if (FileStore.packageName(file).equals(packageName)) return true; var star = packageName + ".*"; for (var i : readImports(file)) { @@ -273,8 +253,8 @@ public Path findTypeDeclaration(String className) { if (fastFind != NOT_FOUND) return fastFind; // In principle, the slow path can be skipped in many cases. // If we're spending a lot of time in findTypeDeclaration, this would be a good optimization. - var packageName = packageName(className); - var simpleName = simpleName(className); + var packageName = Extractors.packageName(className); + var simpleName = Extractors.simpleName(className); for (var f : FileStore.list(packageName)) { if (containsWord(f, simpleName) && containsType(f, className)) { return f; @@ -301,8 +281,8 @@ private Path findPublicTypeDeclaration(String className) { @Override public Path[] findTypeReferences(String className) { - var packageName = packageName(className); - var simpleName = simpleName(className); + var packageName = Extractors.packageName(className); + var simpleName = Extractors.simpleName(className); var candidates = new ArrayList(); for (var f : FileStore.all()) { if (containsWord(f, packageName) && containsImport(f, className) && containsWord(f, simpleName)) { diff --git a/src/main/java/org/javacs/completion/CompletionProvider.java b/src/main/java/org/javacs/completion/CompletionProvider.java index 709cec91..dad23820 100644 --- a/src/main/java/org/javacs/completion/CompletionProvider.java +++ b/src/main/java/org/javacs/completion/CompletionProvider.java @@ -2,6 +2,7 @@ import com.sun.source.tree.ClassTree; import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ImportTree; import com.sun.source.tree.MemberReferenceTree; import com.sun.source.tree.MemberSelectTree; import com.sun.source.tree.MethodTree; @@ -15,13 +16,16 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; import java.util.logging.Logger; +import java.util.stream.Collectors; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; @@ -35,6 +39,7 @@ import org.javacs.CompileTask; import org.javacs.CompilerProvider; import org.javacs.CompletionData; +import org.javacs.Extractors; import org.javacs.FileStore; import org.javacs.JsonHelper; import org.javacs.ParseTask; @@ -45,6 +50,8 @@ import org.javacs.lsp.CompletionItemKind; import org.javacs.lsp.CompletionList; import org.javacs.lsp.InsertTextFormat; +import org.javacs.lsp.TextEdit; +import org.javacs.rewrite.AddImport; public class CompletionProvider { private final CompilerProvider compiler; @@ -336,9 +343,19 @@ private void addClassNames(CompilationUnitTree root, String partial, CompletionL var packageName = Objects.toString(root.getPackageName(), ""); var uniques = new HashSet(); var previousSize = list.items.size(); + + var fileImports = + root.getImports() + .stream() + .map(ImportTree::getQualifiedIdentifier) + .map(Tree::toString) + .collect(Collectors.toSet()); + + var filePath = Path.of(root.getSourceFile().toUri()); + for (var className : compiler.packagePrivateTopLevelTypes(packageName)) { if (!StringSearch.matchesPartialName(className, partial)) continue; - list.items.add(classItem(className)); + list.items.add(classItem(fileImports, filePath, className)); uniques.add(className); } for (var className : compiler.publicTopLevelTypes()) { @@ -348,9 +365,10 @@ private void addClassNames(CompilationUnitTree root, String partial, CompletionL list.isIncomplete = true; break; } - list.items.add(classItem(className)); + list.items.add(classItem(fileImports, filePath, className)); uniques.add(className); } + LOG.info("...found " + (list.items.size() - previousSize) + " class names"); } @@ -579,7 +597,13 @@ private CompletionItem packageItem(String name) { return i; } + // This version does not add an additionalTextEdit to add the import statement. Useful for if + // the completion is for an import statement (which does not need an edit to add itself). private CompletionItem classItem(String className) { + return classItem(Collections.emptySet(), null, className); + } + + private CompletionItem classItem(Set fileImports, Path path, String className) { var i = new CompletionItem(); i.label = simpleName(className).toString(); i.kind = CompletionItemKind.Class; @@ -587,9 +611,20 @@ private CompletionItem classItem(String className) { var data = new CompletionData(); data.className = className; i.data = JsonHelper.GSON.toJsonTree(data); + i.additionalTextEdits = checkForImports(fileImports, path, className); return i; } + private List checkForImports(Set fileImports, Path path, String className) { + final String star = Extractors.packageName(className) + ".*"; + if (fileImports.contains(className) || fileImports.contains(star)) { + return null; + } + + AddImport addImport = new AddImport(path, className); + return List.of(addImport.rewrite(compiler).get(path)); + } + private CompletionItem snippetItem(String label, String snippet) { var i = new CompletionItem(); i.label = label;