diff options
-rw-r--r-- | TODOS.md | 2 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavaCompilerService.java | 246 | ||||
-rw-r--r-- | src/main/java/org/javacs/TemplatePrinter.java | 136 | ||||
-rw-r--r-- | src/test/java/org/javacs/CompletionsTest.java | 8 | ||||
-rw-r--r-- | src/test/test-project/workspace/src/org/javacs/example/AutocompleteOverride.java | 10 |
5 files changed, 323 insertions, 79 deletions
@@ -8,7 +8,7 @@ - NameOfClass... default constructor initializing final fields - im[plements] - Annotation fields -- @Override [method body from super] +- cc should match CamelCase ## Navigation - Go-to-subclasses diff --git a/src/main/java/org/javacs/JavaCompilerService.java b/src/main/java/org/javacs/JavaCompilerService.java index c1915fb..1935459 100644 --- a/src/main/java/org/javacs/JavaCompilerService.java +++ b/src/main/java/org/javacs/JavaCompilerService.java @@ -288,6 +288,64 @@ public class JavaCompilerService { return new FindSmallest().find(cache.root); } + private List<ExecutableElement> virtualMethods(DeclaredType type) { + var result = new ArrayList<ExecutableElement>(); + for (var member : type.asElement().getEnclosedElements()) { + if (member instanceof ExecutableElement) { + var method = (ExecutableElement) member; + if (!method.getSimpleName().contentEquals("<init>") && !method.getModifiers().contains(Modifier.STATIC)) { + result.add(method); + } + } + } + return result; + } + + private TypeMirror enclosingClass(URI file, String contents, int line, int character) { + recompile(file, contents, line, character); + + var trees = Trees.instance(cache.task); + var path = path(file, line, character); + while (!(path.getLeaf() instanceof ClassTree)) path = path.getParentPath(); + var enclosingClass = trees.getElement(path); + + return enclosingClass.asType(); + } + + private List<ExecutableElement> thisMethods(URI file, String contents, int line, int character) { + var thisType = enclosingClass(file, contents, line, character); + var types = cache.task.getTypes(); + var result = new ArrayList<ExecutableElement>(); + + if (thisType instanceof DeclaredType) { + var type = (DeclaredType) thisType; + result.addAll(virtualMethods(type)); + } + + return result; + } + + private void collectSuperMethods(TypeMirror thisType, List<ExecutableElement> result) { + var types = cache.task.getTypes(); + + for (var superType : types.directSupertypes(thisType)) { + if (superType instanceof DeclaredType) { + var type = (DeclaredType) superType; + result.addAll(virtualMethods(type)); + collectSuperMethods(type, result); + } + } + } + + private List<ExecutableElement> superMethods(URI file, String contents, int line, int character) { + var thisType = enclosingClass(file, contents, line, character); + var result = new ArrayList<ExecutableElement>(); + + collectSuperMethods(thisType, result); + + return result; + } + /** Find all identifiers in scope at line:character */ public List<Element> scopeMembers(URI file, String contents, int line, int character) { recompile(file, contents, line, character); @@ -312,8 +370,8 @@ public class JavaCompilerService { } boolean isThisOrSuper(VariableElement ve) { - var name = ve.getSimpleName().toString(); - return name.equals("this") || name.equals("super"); + var name = ve.getSimpleName(); + return name.contentEquals("this") || name.contentEquals("super"); } // Place each member of `this` or `super` directly into `results` @@ -704,6 +762,34 @@ public class JavaCompilerService { } @Override + public Void visitAnnotation(AnnotationTree node, Void nothing) { + if (containsCursor(node.getAnnotationType())) { + LOG.info("...completing annotation"); + result = new ArrayList<>(); + var id = (IdentifierTree) node.getAnnotationType(); + var partialName = Objects.toString(id.getName(), ""); + // Add @Override, @Test, other simple class names + completeScopeIdentifiers(partialName); + // Add @Override ... snippet + if ("Override".startsWith(partialName)) { + // TODO filter out already-implemented methods using thisMethods + for (var method : superMethods(file, contents, line, character)) { + var mods = method.getModifiers(); + if (mods.contains(Modifier.STATIC) || mods.contains(Modifier.PRIVATE)) continue; + + var label = "Override " + ShortTypePrinter.printMethod(method); + var snippet = "Override\n" + new TemplatePrinter().printMethod(method) + " {\n $0\n}"; + var override = Completion.ofSnippet(label, snippet); + result.add(override); + } + } + } else { + super.visitAnnotation(node, nothing); + } + return null; + } + + @Override public Void visitIdentifier(IdentifierTree node, Void nothing) { super.visitIdentifier(node, nothing); @@ -754,88 +840,92 @@ public class JavaCompilerService { } } } - var startsWithUpperCase = partialName.length() > 0 && Character.isUpperCase(partialName.charAt(0)); - var alreadyImported = new HashSet<String>(); - // Add names that have already been imported - for (var m : scopeMembers(file, contents, line, character)) { - if (m.getSimpleName().toString().startsWith(partialName)) { - result.add(Completion.ofElement(m)); - - if (m instanceof TypeElement && startsWithUpperCase) { - var t = (TypeElement) m; - alreadyImported.add(t.getQualifiedName().toString()); - } + completeScopeIdentifiers(partialName); + } + return null; + } + + private void completeScopeIdentifiers(String partialName) { + var startsWithUpperCase = partialName.length() > 0 && Character.isUpperCase(partialName.charAt(0)); + var alreadyImported = new HashSet<String>(); + // Add names that have already been imported + for (var m : scopeMembers(file, contents, line, character)) { + if (m.getSimpleName().toString().startsWith(partialName)) { + result.add(Completion.ofElement(m)); + + if (m instanceof TypeElement && startsWithUpperCase) { + var t = (TypeElement) m; + alreadyImported.add(t.getQualifiedName().toString()); } } - // Add names of classes that haven't been imported - if (startsWithUpperCase) { - var packageName = Objects.toString(parse.getPackageName(), ""); - Predicate<String> matchesPartialName = - qualifiedName -> { - var className = Parser.lastName(qualifiedName); - return className.startsWith(partialName); - }; - Predicate<String> notAlreadyImported = className -> !alreadyImported.contains(className); - var fromJdk = - jdkClasses - .classes() - .stream() - .filter(matchesPartialName) - .filter(notAlreadyImported) - .filter(c -> jdkClasses.isAccessibleFromPackage(c, packageName)); - var fromClasspath = - classPathClasses - .classes() - .stream() - .filter(matchesPartialName) - .filter(notAlreadyImported) - .filter(c -> classPathClasses.isAccessibleFromPackage(c, packageName)); - Function<Path, Stream<String>> classesInDir = dir -> { - Predicate<Path> matchesFileName = - file -> file.getFileName().toString().startsWith(partialName); - Predicate<Path> isPublic = + } + // Add names of classes that haven't been imported + if (startsWithUpperCase) { + var packageName = Objects.toString(parse.getPackageName(), ""); + Predicate<String> matchesPartialName = + qualifiedName -> { + var className = Parser.lastName(qualifiedName); + return className.startsWith(partialName); + }; + Predicate<String> notAlreadyImported = className -> !alreadyImported.contains(className); + var fromJdk = + jdkClasses + .classes() + .stream() + .filter(matchesPartialName) + .filter(notAlreadyImported) + .filter(c -> jdkClasses.isAccessibleFromPackage(c, packageName)); + var fromClasspath = + classPathClasses + .classes() + .stream() + .filter(matchesPartialName) + .filter(notAlreadyImported) + .filter(c -> classPathClasses.isAccessibleFromPackage(c, packageName)); + Function<Path, Stream<String>> classesInDir = dir -> { + Predicate<Path> matchesFileName = + file -> file.getFileName().toString().startsWith(partialName); + Predicate<Path> isPublic = + file -> { + var fileName = file.getFileName().toString(); + if (!fileName.endsWith(".java")) return false; + var simpleName = fileName.substring(0, fileName.length() - ".java".length()); + Stream<String> lines; + try { + lines = Files.lines(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + return lines.anyMatch(line -> line.matches(".*public\\s+class\\s+" + simpleName + ".*")); + }; + Function<Path, String> qualifiedName = file -> { - var fileName = file.getFileName().toString(); - if (!fileName.endsWith(".java")) return false; - var simpleName = fileName.substring(0, fileName.length() - ".java".length()); - Stream<String> lines; - try { - lines = Files.lines(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - return lines.anyMatch(line -> line.matches(".*public\\s+class\\s+" + simpleName + ".*")); + var relative = dir.relativize(file).toString().replace('/', '.'); + if (!relative.endsWith(".java")) return "??? " + relative + " does not end in .java"; + return relative.substring(0, relative.length() - ".java".length()); }; - Function<Path, String> qualifiedName = - file -> { - var relative = dir.relativize(file).toString().replace('/', '.'); - if (!relative.endsWith(".java")) return "??? " + relative + " does not end in .java"; - return relative.substring(0, relative.length() - ".java".length()); - }; - return javaSourcesInDir(dir).filter(matchesFileName).filter(isPublic).map(qualifiedName); - }; - var fromSourcePath = - sourcePath.stream() - .flatMap(classesInDir) - .filter(notAlreadyImported); - Consumer<Stream<String>> addCompletions = qualifiedNames -> { - Iterable<String> it = qualifiedNames::iterator; - for (var name : it) { - if (result.size() >= limitHint) { - isIncomplete = true; - return; - } else { - var completion = Completion.ofNotImportedClass(name); - result.add(completion); - } + return javaSourcesInDir(dir).filter(matchesFileName).filter(isPublic).map(qualifiedName); + }; + var fromSourcePath = + sourcePath.stream() + .flatMap(classesInDir) + .filter(notAlreadyImported); + Consumer<Stream<String>> addCompletions = qualifiedNames -> { + Iterable<String> it = qualifiedNames::iterator; + for (var name : it) { + if (result.size() >= limitHint) { + isIncomplete = true; + return; + } else { + var completion = Completion.ofNotImportedClass(name); + result.add(completion); } - }; - addCompletions.accept(fromJdk); - addCompletions.accept(fromClasspath); - addCompletions.accept(fromSourcePath); - } + } + }; + addCompletions.accept(fromJdk); + addCompletions.accept(fromClasspath); + addCompletions.accept(fromSourcePath); } - return null; } @Override diff --git a/src/main/java/org/javacs/TemplatePrinter.java b/src/main/java/org/javacs/TemplatePrinter.java new file mode 100644 index 0000000..a7892ef --- /dev/null +++ b/src/main/java/org/javacs/TemplatePrinter.java @@ -0,0 +1,136 @@ +package org.javacs; + +import java.util.HashMap; +import java.util.Map; +import java.util.StringJoiner; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.*; +import javax.lang.model.util.AbstractTypeVisitor8; + +class TemplatePrinter extends AbstractTypeVisitor8<String, Void> { + private final Logger LOG = Logger.getLogger("main"); + + private Map<TypeMirror, Integer> parameters = new HashMap<>(); + + private int parameter(TypeMirror t) { + if (parameters.containsKey(t)) { + return parameters.get(t); + } else { + parameters.put(t, parameters.size() + 1); + return parameters.get(t); + } + } + + String print(TypeMirror type) { + return type.accept(this, null); + } + + @Override + public String visitIntersection(IntersectionType t, Void aVoid) { + var types = new StringJoiner(" & "); + for (var b : t.getBounds()) { + types.add(print(b)); + } + return "? extends " + types; + } + + @Override + public String visitUnion(UnionType t, Void aVoid) { + return "UNION"; + } + + @Override + public String visitPrimitive(PrimitiveType t, Void aVoid) { + return t.toString(); + } + + @Override + public String visitNull(NullType t, Void aVoid) { + return "NULL"; + } + + @Override + public String visitArray(ArrayType t, Void aVoid) { + return print(t.getComponentType()) + "[]"; + } + + @Override + public String visitDeclared(DeclaredType t, Void aVoid) { + var result = t.asElement().getSimpleName().toString(); + + if (!t.getTypeArguments().isEmpty()) { + String params = t.getTypeArguments().stream().map(this::print).collect(Collectors.joining(", ")); + + result += "<" + params + ">"; + } + + if (result.matches("java\\.lang\\.\\w+")) return result.substring("java.lang.".length()); + else if (result.startsWith("java\\.util\\.\\w+")) return result.substring("java.util.".length()); + else return result; + } + + @Override + public String visitError(ErrorType t, Void aVoid) { + return "ERROR"; + } + + @Override + public String visitTypeVariable(TypeVariable t, Void aVoid) { + return "$" + parameter(t); + } + + @Override + public String visitWildcard(WildcardType t, Void aVoid) { + return "?"; + } + + @Override + public String visitExecutable(ExecutableType t, Void aVoid) { + return "EXECUTABLE"; + } + + @Override + public String visitNoType(NoType t, Void aVoid) { + return "void"; + } + + private String printArguments(ExecutableElement e) { + var result = new StringJoiner(", "); + for (var p : e.getParameters()) { + var s = new StringBuilder(); + result.add(print(p.asType()) + " " + p.getSimpleName()); + } + return result.toString(); + } + + String printMethod(ExecutableElement m) { + if (m.getSimpleName().contentEquals("<init>")) { + return m.getEnclosingElement().getSimpleName() + "(" + printArguments(m) + ")"; + } else { + if (m.getModifiers().contains(Modifier.STATIC)) return "ERROR " + m.getSimpleName() + " IS STATIC"; + if (m.getModifiers().contains(Modifier.PRIVATE)) return "ERROR " + m.getSimpleName() + " IS PRIVATE"; + + var result = new StringBuilder(); + // public void foo + if (m.getModifiers().contains(Modifier.PUBLIC)) result.append("public "); + if (m.getModifiers().contains(Modifier.PROTECTED)) result.append("protected "); + result.append(print(m.getReturnType())).append(" "); + result.append(m.getSimpleName()); + // (int arg, String other) + result.append("(").append(printArguments(m)).append(")"); + // throws Foo, Bar + if (!m.getThrownTypes().isEmpty()) { + result.append(" throws "); + var types = new StringJoiner(", "); + for (var t : m.getThrownTypes()) { + types.add(print(t)); + } + result.append(types); + } + return result.toString(); + } + } +} diff --git a/src/test/java/org/javacs/CompletionsTest.java b/src/test/java/org/javacs/CompletionsTest.java index 52b1921..c8f01ca 100644 --- a/src/test/java/org/javacs/CompletionsTest.java +++ b/src/test/java/org/javacs/CompletionsTest.java @@ -776,4 +776,12 @@ public class CompletionsTest extends CompletionsBase { assertThat(suggestions, hasItem(startsWith("Override"))); } + + @Test + public void overrideMethod() { + var file = "/org/javacs/example/AutocompleteOverride.java"; + var suggestions = insertText(file, 8, 15); + + assertThat(suggestions, hasItem(containsString("void superMethod() {"))); + } } diff --git a/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOverride.java b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOverride.java new file mode 100644 index 0000000..8211b18 --- /dev/null +++ b/src/test/test-project/workspace/src/org/javacs/example/AutocompleteOverride.java @@ -0,0 +1,10 @@ +package org.javacs.example; + +class AutocompleteOverride { + class Super { + void superMethod() {} + } + class Sub extends Super { + @Overr + } +}
\ No newline at end of file |