diff options
author | George Fraser <george@fivetran.com> | 2017-03-18 22:03:08 -0700 |
---|---|---|
committer | George Fraser <george@fivetran.com> | 2017-03-18 22:03:08 -0700 |
commit | 139784693f4bef6078fdc58e7ce17a9416e1a6c8 (patch) | |
tree | 73fbcc4035849b836a212f2bf7dcb7ce18318010 | |
parent | 9b251a1e2ff52e82132427193d062ece48c266b2 (diff) | |
download | java-language-server-139784693f4bef6078fdc58e7ce17a9416e1a6c8.zip |
Compiles but structure may not be quite right
-rw-r--r-- | src/main/java/org/javacs/JavaLanguageServer.java | 539 | ||||
-rw-r--r-- | src/main/java/org/javacs/JavacHolder.java | 317 | ||||
-rw-r--r-- | src/main/java/org/javacs/StringFileObject.java | 8 | ||||
-rw-r--r-- | src/main/java/org/javacs/SymbolIndex.java | 134 | ||||
-rw-r--r-- | src/main/java/org/javacs/TreePruner.java | 59 |
5 files changed, 569 insertions, 488 deletions
diff --git a/src/main/java/org/javacs/JavaLanguageServer.java b/src/main/java/org/javacs/JavaLanguageServer.java index 9f27d83..f22156d 100644 --- a/src/main/java/org/javacs/JavaLanguageServer.java +++ b/src/main/java/org/javacs/JavaLanguageServer.java @@ -1,14 +1,21 @@ package org.javacs; -import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.code.Symbol; -import com.sun.tools.javac.tree.TreeInfo; -import com.sun.tools.javac.tree.TreeScanner; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.services.*; import org.eclipse.lsp4j.*; +import org.eclipse.lsp4j.services.LanguageClient; +import org.eclipse.lsp4j.services.LanguageServer; +import org.eclipse.lsp4j.services.TextDocumentService; +import org.eclipse.lsp4j.services.WorkspaceService; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; -import javax.tools.*; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaFileObject; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; import java.io.*; import java.net.URI; import java.nio.file.Files; @@ -21,18 +28,14 @@ import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.stream.Collectors; - -import javax.xml.parsers.*; -import javax.xml.xpath.*; -import org.w3c.dom.*; -import org.xml.sax.SAXException; +import java.util.stream.Stream; import static org.javacs.Main.JSON; class JavaLanguageServer implements LanguageServer { private static final Logger LOG = Logger.getLogger("main"); private Path workspaceRoot; - private Map<Path, String> activeDocuments = new HashMap<>(); + private Map<URI, String> activeDocuments = new HashMap<>(); private LanguageClient client; public JavaLanguageServer() { @@ -178,15 +181,11 @@ class JavaLanguageServer implements LanguageServer { try { TextDocumentItem document = params.getTextDocument(); URI uri = URI.create(document.getUri()); - Optional<Path> maybePath = getFilePath(uri); + String text = document.getText(); - maybePath.ifPresent(path -> { - String text = document.getText(); + activeDocuments.put(uri, text); - activeDocuments.put(path, text); - - doLint(path); - }); + doLint(Collections.singleton(uri)); } catch (NoJavaConfigException e) { throw ShowMessageException.warning(e.getMessage(), e); } @@ -196,18 +195,15 @@ class JavaLanguageServer implements LanguageServer { public void didChange(DidChangeTextDocumentParams params) { VersionedTextDocumentIdentifier document = params.getTextDocument(); URI uri = URI.create(document.getUri()); - Optional<Path> path = getFilePath(uri); - if (path.isPresent()) { - for (TextDocumentContentChangeEvent change : params.getContentChanges()) { - if (change.getRange() == null) - activeDocuments.put(path.get(), change.getText()); - else { - String existingText = activeDocuments.get(path.get()); - String newText = patch(existingText, change); + for (TextDocumentContentChangeEvent change : params.getContentChanges()) { + if (change.getRange() == null) + activeDocuments.put(uri, change.getText()); + else { + String existingText = activeDocuments.get(uri); + String newText = patch(existingText, change); - activeDocuments.put(path.get(), newText); - } + activeDocuments.put(uri, newText); } } } @@ -216,30 +212,23 @@ class JavaLanguageServer implements LanguageServer { public void didClose(DidCloseTextDocumentParams params) { TextDocumentIdentifier document = params.getTextDocument(); URI uri = URI.create(document.getUri()); - Optional<Path> path = getFilePath(uri); - - if (path.isPresent()) { - JavacHolder compiler = findCompiler(path.get()); - JavaFileObject file = findFile(compiler, path.get()); - - // Remove from source cache - activeDocuments.remove(path.get()); - } + + // Remove from source cache + activeDocuments.remove(uri); } @Override public void didSave(DidSaveTextDocumentParams params) { TextDocumentIdentifier document = params.getTextDocument(); URI uri = URI.create(document.getUri()); - Optional<Path> maybePath = getFilePath(uri); + // TODO can we just re-line uri? // Re-lint all active documents // // We would prefer to just re-lint the documents that the user can see // But there is no didSwitchTo(document) event, so we have no way of knowing when the user switches between tabs // Therefore, we just re-lint all open editors - if (maybePath.isPresent()) - doLint(activeDocuments.keySet()); + doLint(activeDocuments.keySet()); } }; } @@ -282,56 +271,27 @@ class JavaLanguageServer implements LanguageServer { } } - private Optional<Path> getFilePath(URI uri) { - if (!uri.getScheme().equals("file")) - return Optional.empty(); - else - return Optional.of(Paths.get(uri)); - } - - private void doLint(Path path) { - doLint(Collections.singleton(path)); - } - - private void doLint(Collection<Path> paths) { + private void doLint(Collection<URI> paths) { LOG.info("Lint " + paths); - DiagnosticCollector<JavaFileObject> errors = new DiagnosticCollector<>(); - - Map<JavacConfig, Set<JCTree.JCCompilationUnit>> parsedByConfig = new HashMap<>(); - - // Parse all files and group them by compiler - for (Path path : paths) { - findConfig(path).ifPresent(config -> { - Set<JCTree.JCCompilationUnit> collect = parsedByConfig.computeIfAbsent(config, newCompiler -> new HashSet<>()); - - // Find the relevant compiler - JavacHolder compiler = findCompilerForConfig(config); - - compiler.onError(errors); + Map<JavacConfig, Map<URI, Optional<String>>> files = new HashMap<>(); - // Parse the file - JavaFileObject file = findFile(compiler, path); - JCTree.JCCompilationUnit parsed = compiler.parse(file); - - collect.add(parsed); + for (URI each : paths) { + dir(each).flatMap(this::findConfig).ifPresent(config -> { + files.computeIfAbsent(config, newConfig -> new HashMap<>()).put(each, activeContent(each)); }); } + files.forEach((config, configFiles) -> { + publishDiagnostics(paths, findCompilerForConfig(config).update(configFiles)); + }); + } - for (JavacConfig config : parsedByConfig.keySet()) { - Set<JCTree.JCCompilationUnit> parsed = parsedByConfig.get(config); - JavacHolder compiler = findCompilerForConfig(config); - SymbolIndex index = findIndexForConfig(config); - - compiler.compile(parsed); - - // TODO compiler should do this automatically - for (JCTree.JCCompilationUnit compilationUnit : parsed) - index.update(compilationUnit, compiler.context); - } - - publishDiagnostics(paths, errors); + /** + * Text of file, if it is in the active set + */ + private Optional<String> activeContent(URI file) { + return Optional.ofNullable(activeDocuments.get(file)); } @Override @@ -339,11 +299,12 @@ class JavaLanguageServer implements LanguageServer { return new WorkspaceService() { @Override public CompletableFuture<List<? extends SymbolInformation>> symbol(WorkspaceSymbolParams params) { - List<SymbolInformation> infos = indexCache.values() - .stream() - .flatMap(symbolIndex -> symbolIndex.search(params.getQuery())) - .limit(100) - .collect(Collectors.toList()); + List<SymbolInformation> infos = compilerCache + .values() + .stream() + .flatMap(compiler -> compiler.index.search(params.getQuery())) + .limit(100) + .collect(Collectors.toList()); return CompletableFuture.completedFuture(infos); } @@ -360,13 +321,10 @@ class JavaLanguageServer implements LanguageServer { if (event.getType() == FileChangeType.Deleted) { URI uri = URI.create(event.getUri()); - getFilePath(uri).ifPresent(path -> { - JavacHolder compiler = findCompiler(path); - JavaFileObject file = findFile(compiler, path); - SymbolIndex index = findIndex(path); + activeDocuments.remove(uri); - compiler.clear(file); - index.clear(file.toUri()); + findCompiler(uri).ifPresent(compiler -> { + compiler.delete(uri); }); } } @@ -378,10 +336,10 @@ class JavaLanguageServer implements LanguageServer { }; } - private void publishDiagnostics(Collection<Path> paths, DiagnosticCollector<JavaFileObject> errors) { + private void publishDiagnostics(Collection<URI> paths, DiagnosticCollector<JavaFileObject> errors) { Map<URI, PublishDiagnosticsParams> files = new HashMap<>(); - paths.forEach(p -> files.put(p.toUri(), newPublishDiagnostics(p.toUri()))); + paths.forEach(p -> files.put(p, newPublishDiagnostics(p))); errors.getDiagnostics().forEach(error -> { if (error.getStartPosition() != javax.tools.Diagnostic.NOPOS) { @@ -438,15 +396,24 @@ class JavaLanguageServer implements LanguageServer { /** * Look for a configuration in a parent directory of uri */ - private JavacHolder findCompiler(Path path) { + private Optional<JavacHolder> findCompiler(URI uri) { if (testJavac.isPresent()) - return testJavac.get(); + return testJavac; + else + return dir(uri) + .flatMap(this::findConfig) + .map(this::findCompilerForConfig); + } - Path dir = path.getParent(); - - return findConfig(dir) - .map(this::findCompilerForConfig) - .orElseThrow(() -> new NoJavaConfigException(path)); + private static Optional<Path> dir(URI uri) { + return file(uri).map(path -> path.getParent()); + } + + private static Optional<Path> file(URI uri) { + if (!uri.getScheme().equals("file")) + return Optional.empty(); + else + return Optional.of(Paths.get(uri)); } private JavacHolder findCompilerForConfig(JavacConfig config) { @@ -462,24 +429,6 @@ class JavaLanguageServer implements LanguageServer { c.outputDirectory); } - private Map<JavacConfig, SymbolIndex> indexCache = new HashMap<>(); - - private SymbolIndex findIndex(Path path) { - Path dir = path.getParent(); - Optional<JavacConfig> config = findConfig(dir); - Optional<SymbolIndex> index = config.map(this::findIndexForConfig); - - return index.orElseThrow(() -> new NoJavaConfigException(path)); - } - - private SymbolIndex findIndexForConfig(JavacConfig config) { - return indexCache.computeIfAbsent(config, this::newIndex); - } - - private SymbolIndex newIndex(JavacConfig c) { - return new SymbolIndex(c.classPath, c.sourcePath, c.outputDirectory); - } - // TODO invalidate cache when VSCode notifies us config file has changed private Map<Path, Optional<JavacConfig>> configCache = new HashMap<>(); @@ -658,13 +607,6 @@ class JavaLanguageServer implements LanguageServer { } } - private JavaFileObject findFile(JavacHolder compiler, Path path) { - if (activeDocuments.containsKey(path)) - return new StringFileObject(activeDocuments.get(path), path); - else - return compiler.fileManager.getRegularFile(path.toFile()); - } - private Range position(javax.tools.Diagnostic<? extends JavaFileObject> error) { // Compute start position Position start = new Position(); @@ -718,121 +660,36 @@ class JavaLanguageServer implements LanguageServer { private List<? extends Location> findReferences(ReferenceParams params) { URI uri = URI.create(params.getTextDocument().getUri()); + Optional<String> content = activeContent(uri); int line = params.getPosition().getLine(); int character = params.getPosition().getCharacter(); - List<Location> result = new ArrayList<>(); - - getFilePath(uri).ifPresent(path -> { - JCTree.JCCompilationUnit compilationUnit = findTree(path); - - findSymbol(compilationUnit, line, character).ifPresent(symbol -> { - if (SymbolIndex.shouldIndex(symbol)) { - SymbolIndex index = findIndex(path); - - index.references(symbol).forEach(result::add); - } - else { - compilationUnit.accept(new TreeScanner() { - @Override - public void visitSelect(JCTree.JCFieldAccess tree) { - super.visitSelect(tree); - - if (tree.sym != null && tree.sym.equals(symbol)) - result.add(SymbolIndex.location(tree, compilationUnit)); - } - - @Override - public void visitReference(JCTree.JCMemberReference tree) { - super.visitReference(tree); + long cursor = findOffset(uri, line, character); - if (tree.sym != null && tree.sym.equals(symbol)) - result.add(SymbolIndex.location(tree, compilationUnit)); - } - - @Override - public void visitIdent(JCTree.JCIdent tree) { - super.visitIdent(tree); - - if (tree.sym != null && tree.sym.equals(symbol)) - result.add(SymbolIndex.location(tree, compilationUnit)); - } - }); - } - }); - }); - - return result; + return findCompiler(uri) + .map(compiler -> compiler.findReferences(uri, content, cursor)) + .orElse(Collections.emptyList()); } private List<? extends SymbolInformation> findDocumentSymbols(DocumentSymbolParams params) { URI uri = URI.create(params.getTextDocument().getUri()); - return getFilePath(uri).map(path -> { - SymbolIndex index = findIndex(path); - List<? extends SymbolInformation> found = index.allInFile(uri).collect(Collectors.toList()); - - return found; - }).orElse(Collections.emptyList()); - } - - private JCTree.JCCompilationUnit findTree(Path path) { - JavacHolder compiler = findCompiler(path); - SymbolIndex index = findIndex(path); - JavaFileObject file = findFile(compiler, path); - - compiler.onError(err -> {}); - - JCTree.JCCompilationUnit tree = compiler.parse(file); - - compiler.compile(Collections.singleton(tree)); - - // TODO compiler should do this automatically - index.update(tree, compiler.context); - - return tree; - } - - public Optional<Symbol> findSymbol(JCTree.JCCompilationUnit tree, int line, int character) { - JavaFileObject file = tree.getSourceFile(); - - return getFilePath(file.toUri()).flatMap(path -> { - JavacHolder compiler = findCompiler(path); - long cursor = findOffset(file, line, character); - SymbolUnderCursorVisitor visitor = new SymbolUnderCursorVisitor(file, cursor, compiler.context); - - tree.accept(visitor); - - return visitor.found; - }); + return findCompiler(uri) + .map(compiler -> compiler.index.allInFile(uri)) + .orElse(Stream.empty()) + .collect(Collectors.toList()); } public List<? extends Location> gotoDefinition(TextDocumentPositionParams position) { URI uri = URI.create(position.getTextDocument().getUri()); + Optional<String> content = activeContent(uri); int line = position.getPosition().getLine(); int character = position.getPosition().getCharacter(); - List<Location> result = new ArrayList<>(); - - getFilePath(uri).ifPresent(path -> { - JCTree.JCCompilationUnit compilationUnit = findTree(path); + long cursor = findOffset(uri, line, character); - findSymbol(compilationUnit, line, character).ifPresent(symbol -> { - if (SymbolIndex.shouldIndex(symbol)) { - SymbolIndex index = findIndex(path); - - index.findSymbol(symbol).ifPresent(info -> { - result.add(info.getLocation()); - }); - } - else { - JCTree symbolTree = TreeInfo.declarationFor(symbol, compilationUnit); - - if (symbolTree != null) - result.add(SymbolIndex.location(symbolTree, compilationUnit)); - } - }); - }); - - return result; + return findCompiler(uri) + .flatMap(compiler -> compiler.gotoDefinition(uri, content, cursor)) + .map(Collections::singletonList) + .orElse(Collections.emptyList()); } /** @@ -903,168 +760,104 @@ class JavaLanguageServer implements LanguageServer { return p; } - private static long findOffset(JavaFileObject file, int targetLine, int targetCharacter) { - try (Reader in = file.openReader(true)) { - long offset = 0; - int line = 0; - int character = 0; + private static long findOffset(URI file, int targetLine, int targetCharacter) { + return file(file).map(path -> { + try (Reader in = Files.newBufferedReader(path)) { + long offset = 0; + int line = 0; + int character = 0; - while (line < targetLine) { - int next = in.read(); + while (line < targetLine) { + int next = in.read(); - if (next < 0) - return offset; - else { - offset++; + if (next < 0) + return offset; + else { + offset++; - if (next == '\n') - line++; + if (next == '\n') + line++; + } } - } - while (character < targetCharacter) { - int next = in.read(); + while (character < targetCharacter) { + int next = in.read(); - if (next < 0) - return offset; - else { - offset++; - character++; + if (next < 0) + return offset; + else { + offset++; + character++; + } } - } - return offset; - } catch (IOException e) { - throw ShowMessageException.error(e.getMessage(), e); - } + return offset; + } catch (IOException e) { + throw ShowMessageException.error(e.getMessage(), e); + } + }).orElseThrow(() -> ShowMessageException.error("Can't find " + targetLine + ":" + targetCharacter + " in " + file, null)); } private Hover doHover(TextDocumentPositionParams position) { - Hover result = new Hover(); - List<String> contents = new ArrayList<>(); - - result.setContents(contents); - URI uri = URI.create(position.getTextDocument().getUri()); + Optional<String> content = activeContent(uri); int line = position.getPosition().getLine(); int character = position.getPosition().getCharacter(); - - getFilePath(uri).ifPresent(path -> { - JCTree.JCCompilationUnit compilationUnit = findTree(path); - - findSymbol(compilationUnit, line, character).ifPresent(symbol -> { - switch (symbol.getKind()) { - case PACKAGE: - contents.add("package " + symbol.getQualifiedName()); - - break; - case ENUM: - contents.add("enum " + symbol.getQualifiedName()); - - break; - case CLASS: - contents.add("class " + symbol.getQualifiedName()); - - break; - case ANNOTATION_TYPE: - contents.add("@interface " + symbol.getQualifiedName()); - - break; - case INTERFACE: - contents.add("interface " + symbol.getQualifiedName()); - - break; - case METHOD: - case CONSTRUCTOR: - case STATIC_INIT: - case INSTANCE_INIT: - Symbol.MethodSymbol method = (Symbol.MethodSymbol) symbol; - String signature = AutocompleteVisitor.methodSignature(method); - String returnType = ShortTypePrinter.print(method.getReturnType()); - - contents.add(returnType + " " + signature); - - break; - case PARAMETER: - case LOCAL_VARIABLE: - case EXCEPTION_PARAMETER: - case ENUM_CONSTANT: - case FIELD: - contents.add(ShortTypePrinter.print(symbol.type)); - - break; - case TYPE_PARAMETER: - case OTHER: - case RESOURCE_VARIABLE: - break; - } - }); - }); - - return result; - } - - public CompletionList autocomplete(TextDocumentPositionParams position) { - CompletionList result = new CompletionList(); - - result.setIsIncomplete(false); - result.setItems(new ArrayList<>()); - - Optional<Path> maybePath = getFilePath(URI.create(position.getTextDocument().getUri())); - - if (maybePath.isPresent()) { - Path path = maybePath.get(); - DiagnosticCollector<JavaFileObject> errors = new DiagnosticCollector<>(); - JavacHolder compiler = findCompiler(path); - JavaFileObject file = findFile(compiler, path); - long cursor = findOffset(file, position.getPosition().getLine(), position.getPosition().getCharacter()); - JavaFileObject withSemi = withSemicolonAfterCursor(file, path, cursor); - AutocompleteVisitor autocompleter = new AutocompleteVisitor(withSemi, cursor, compiler.context); - - compiler.onError(errors); - - JCTree.JCCompilationUnit ast = compiler.parse(withSemi); - - // Remove all statements after the cursor - // There are often parse errors after the cursor, which can generate unrecoverable type errors - ast.accept(new AutocompletePruner(withSemi, cursor, compiler.context)); - - compiler.compile(Collections.singleton(ast)); - - ast.accept(autocompleter); - - result.getItems().addAll(autocompleter.suggestions); + long cursor = findOffset(uri, line, character); + + return findCompiler(uri) + .flatMap(compiler -> compiler.symbolAt(uri, content, cursor)) + .flatMap(JavaLanguageServer::hoverText) + .map(text -> new Hover(Collections.singletonList(text), null)) + .orElse(new Hover()); + } + + private static Optional<String> hoverText(Symbol symbol) { + switch (symbol.getKind()) { + case PACKAGE: + return Optional.of("package " + symbol.getQualifiedName()); + case ENUM: + return Optional.of("enum " + symbol.getQualifiedName()); + case CLASS: + return Optional.of("class " + symbol.getQualifiedName()); + case ANNOTATION_TYPE: + return Optional.of("@interface " + symbol.getQualifiedName()); + case INTERFACE: + return Optional.of("interface " + symbol.getQualifiedName()); + case METHOD: + case CONSTRUCTOR: + case STATIC_INIT: + case INSTANCE_INIT: + Symbol.MethodSymbol method = (Symbol.MethodSymbol) symbol; + String signature = AutocompleteVisitor.methodSignature(method); + String returnType = ShortTypePrinter.print(method.getReturnType()); + + return Optional.of(returnType + " " + signature); + case PARAMETER: + case LOCAL_VARIABLE: + case EXCEPTION_PARAMETER: + case ENUM_CONSTANT: + case FIELD: + return Optional.of(ShortTypePrinter.print(symbol.type)); + case TYPE_PARAMETER: + case OTHER: + case RESOURCE_VARIABLE: + default: + return Optional.empty(); } - - return result; } - /** - * Insert ';' after the users cursor so we recover from parse errors in a helpful way when doing autocomplete. - */ - private JavaFileObject withSemicolonAfterCursor(JavaFileObject file, Path path, long cursor) { - try (Reader reader = file.openReader(true)) { - StringBuilder acc = new StringBuilder(); - - for (int i = 0; i < cursor; i++) { - int next = reader.read(); - - if (next == -1) - throw new RuntimeException("End of file " + file + " before cursor " + cursor); - - acc.append((char) next); - } - - acc.append(";"); - - for (int next = reader.read(); next > 0; next = reader.read()) { - acc.append((char) next); - } + public CompletionList autocomplete(TextDocumentPositionParams position) { + URI uri = URI.create(position.getTextDocument().getUri()); + Optional<String> content = activeContent(uri); + int line = position.getPosition().getLine(); + int character = position.getPosition().getCharacter(); + long cursor = findOffset(uri, line, character); + List<CompletionItem> items = findCompiler(uri) + .map(compiler -> compiler.autocomplete(uri, content, cursor)) + .orElse(Collections.emptyList()); - return new StringFileObject(acc.toString(), path); - } catch (IOException e) { - throw ShowMessageException.error("Error reading " + file, e); - } + return new CompletionList(false, items); } public void installClient(LanguageClient client) { diff --git a/src/main/java/org/javacs/JavacHolder.java b/src/main/java/org/javacs/JavacHolder.java index c98b438..d0ac625 100644 --- a/src/main/java/org/javacs/JavacHolder.java +++ b/src/main/java/org/javacs/JavacHolder.java @@ -1,11 +1,11 @@ package org.javacs; import com.google.common.base.Joiner; -import com.google.common.collect.ImmutableList; import com.sun.source.util.TaskEvent; import com.sun.source.util.TaskListener; import com.sun.tools.javac.api.JavacTrees; import com.sun.tools.javac.api.MultiTaskListener; +import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; import com.sun.tools.javac.code.Types; import com.sun.tools.javac.comp.*; @@ -13,12 +13,20 @@ import com.sun.tools.javac.file.JavacFileManager; import com.sun.tools.javac.main.JavaCompiler; import com.sun.tools.javac.parser.FuzzyParserFactory; import com.sun.tools.javac.tree.JCTree; -import com.sun.tools.javac.tree.TreeScanner; +import com.sun.tools.javac.tree.TreeInfo; import com.sun.tools.javac.util.*; - -import javax.tools.*; -import java.io.*; +import org.eclipse.lsp4j.CompletionItem; +import org.eclipse.lsp4j.Location; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.DiagnosticListener; +import javax.tools.JavaFileObject; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; import java.lang.reflect.Field; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.FileTime; @@ -29,6 +37,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; /** * Maintains a reference to a Java compiler, @@ -38,26 +47,38 @@ import java.util.logging.Logger; */ public class JavacHolder { private static final Logger LOG = Logger.getLogger("main"); - /** Where this javac looks for library .class files */ + /** + * Where this javac looks for library .class files + */ public final Set<Path> classPath; - /** Where this javac looks for .java source files */ + /** + * Where this javac looks for .java source files + */ public final Set<Path> sourcePath; - /** Where this javac places generated .class files */ + /** + * Where this javac places generated .class files + */ public final Path outputDirectory; - // javac places all of its internal state into this Context object, - // which is basically a Map<String, Object> + /** + * javac places all of its internal state into this Context object, + * which is basically a Map<String, Object> + */ public final Context context = new Context(); - // Error reporting initially goes nowhere - // When we want to report errors back to VS Code, we'll replace this with something else - private DiagnosticListener<JavaFileObject> errorsDelegate = diagnostic -> {}; - // javac isn't friendly to swapping out the error-reporting DiagnosticListener, - // so we install this intermediate DiagnosticListener, which forwards to errorsDelegate - private final DiagnosticListener<JavaFileObject> errors = diagnostic -> { - errorsDelegate.report(diagnostic); + /** + * Error reporting initially goes nowhere. + * We will replace this with a function that collects errors so we can report all the errors associated with a file at once. + */ + private DiagnosticListener<JavaFileObject> onErrorDelegate = diagnostic -> {}; + /** + * javac isn't friendly to swapping out the error-reporting DiagnosticListener, + * so we install this intermediate DiagnosticListener, which forwards to errorsDelegate + */ + private final DiagnosticListener<JavaFileObject> onError = diagnostic -> { + onErrorDelegate.report(diagnostic); }; { - context.put(DiagnosticListener.class, errors); + context.put(DiagnosticListener.class, onError); } // Sets command-line options @@ -105,6 +126,18 @@ public class JavacHolder { private final JavacTrees trees = JavacTrees.instance(context); private final Types types = Types.instance(context); + // Set up SymbolIndex + + /** + * Index of symbols that gets updated every time you call update + */ + public final SymbolIndex index = new SymbolIndex(this); + + /** + * Completes when initial index is done. Useful for testing. + */ + public final CompletableFuture<Void> initialIndexComplete; + public JavacHolder(Set<Path> classPath, Set<Path> sourcePath, Path outputDirectory) { this.classPath = Collections.unmodifiableSet(classPath); this.sourcePath = Collections.unmodifiableSet(sourcePath); @@ -114,6 +147,14 @@ public class JavacHolder { options.put("-sourcepath", Joiner.on(File.pathSeparator).join(sourcePath)); options.put("-d", outputDirectory.toString()); + logStartStopEvents(); + ensureOutputDirectory(outputDirectory); + clearOutputDirectory(outputDirectory); + + initialIndexComplete = startIndexingSourcePath(); + } + + private void logStartStopEvents() { MultiTaskListener.instance(context).add(new TaskListener() { @Override public void started(TaskEvent e) { @@ -129,9 +170,63 @@ public class JavacHolder { JCTree.JCCompilationUnit unit = (JCTree.JCCompilationUnit) e.getCompilationUnit(); } }); + } - ensureOutputDirectory(outputDirectory); - clearOutputDirectory(outputDirectory); + /** + * Index exported declarations and references for all files on the source path + * This may take a while, so we'll do it on an extra thread + */ + private CompletableFuture<Void> startIndexingSourcePath() { + CompletableFuture<Void> done = new CompletableFuture<>(); + Thread worker = new Thread("InitialIndex") { + List<JCTree.JCCompilationUnit> parsed = new ArrayList<>(); + List<Path> paths = new ArrayList<>(); + + @Override + public void run() { + // Parse each file + sourcePath.forEach(s -> parseAll(s, parsed, paths)); + + // Compile all parsed files + compile(parsed); + + parsed.forEach(index::update); + + // TODO minimize memory use during this process + // Instead of doing parse-all / compile-all, + // queue all files, then do parse / compile on each + // If invoked correctly, javac should avoid reparsing the same file twice + // Then, use the same mechanism as the desugar / generate phases to remove method bodies, + // to reclaim memory as we go + + done.complete(null); + + // TODO verify that compiler and all its resources get destroyed + } + + /** + * Look for .java files and invalidate them + */ + private void parseAll(Path path, List<JCTree.JCCompilationUnit> trees, List<Path> paths) { + if (Files.isDirectory(path)) try { + Files.list(path).forEach(p -> parseAll(p, trees, paths)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + else if (path.getFileName().toString().endsWith(".java")) { + LOG.info("Index " + path); + + JavaFileObject file = fileManager.getRegularFile(path.toFile()); + + trees.add(parse(file)); + paths.add(path); + } + } + }; + + worker.start(); + + return done; } /** @@ -167,14 +262,149 @@ public class JavacHolder { } /** - * Send all errors to callback, replacing any existing callback + * Suggest possible completions + * + * @param file Path to file + * @param textContent Current text of file, if available + * @param cursor Offset in file where the cursor is + */ + public List<CompletionItem> autocomplete(URI file, Optional<String> textContent, long cursor) { + JavaFileObject object = findFile(file, textContent); + + object = TreePruner.putSemicolonAfterCursor(object, file, cursor); + + JCTree.JCCompilationUnit tree = parse(object); + + // Remove all statements after the cursor + // There are often parse errors after the cursor, which can generate unrecoverable type errors + new TreePruner(tree, context).removeStatementsAfterCursor(cursor); + + compile(Collections.singleton(tree)); + + return doAutocomplete(object, tree, cursor); + } + + private List<CompletionItem> doAutocomplete(JavaFileObject object, JCTree.JCCompilationUnit pruned, long cursor) { + AutocompleteVisitor autocompleter = new AutocompleteVisitor(object, cursor, context); + + pruned.accept(autocompleter); + + return autocompleter.suggestions; + } + + /** + * Find references to the symbol at the cursor, if there is a symbol at the cursor + * + * @param file Path to file + * @param textContent Current text of file, if available + * @param cursor Offset in file where the cursor is + */ + public List<Location> findReferences(URI file, Optional<String> textContent, long cursor) { + JCTree.JCCompilationUnit tree = findTree(file, textContent); + + return findSymbol(tree, cursor) + .map(s -> doFindReferences(s, tree)) + .orElse(Collections.emptyList()); + } + + private List<Location> doFindReferences(Symbol symbol, JCTree.JCCompilationUnit compilationUnit) { + if (SymbolIndex.shouldIndex(symbol)) + return index.references(symbol).collect(Collectors.toList()); + else { + return SymbolIndex.nonIndexedReferences(symbol, compilationUnit); + } + } + + public Optional<Location> gotoDefinition(URI file, Optional<String> textContent, long cursor) { + JCTree.JCCompilationUnit tree = findTree(file, textContent); + + return findSymbol(tree, cursor) + .flatMap(s -> doGotoDefinition(s, tree)); + } + + private Optional<Location> doGotoDefinition(Symbol symbol, JCTree.JCCompilationUnit compilationUnit) { + if (SymbolIndex.shouldIndex(symbol)) + return index.findSymbol(symbol).map(info -> info.getLocation()); + else { + // TODO isn't this ever empty? + JCTree symbolTree = TreeInfo.declarationFor(symbol, compilationUnit); + + return Optional.of(SymbolIndex.location(symbolTree, compilationUnit)); + } + } + + private JCTree.JCCompilationUnit findTree(URI file, Optional<String> textContent) { + JCTree.JCCompilationUnit tree = parse(findFile(file, textContent)); + + compile(Collections.singleton(tree)); + + index.update(tree, context); + + return tree; + } + + private Optional<Symbol> findSymbol(JCTree.JCCompilationUnit tree, long cursor) { + JavaFileObject file = tree.getSourceFile(); + SymbolUnderCursorVisitor visitor = new SymbolUnderCursorVisitor(file, cursor, context); + + tree.accept(visitor); + + return visitor.found; + } + + public Optional<Symbol> symbolAt(URI file, Optional<String> textContent, long cursor) { + JCTree.JCCompilationUnit tree = findTree(file, textContent); + + return findSymbol(tree, cursor); + } + + /** + * Clear files and all their dependents, recompile, update the index, and report any errors. */ - public void onError(DiagnosticListener<JavaFileObject> callback) { - errorsDelegate = callback; + public DiagnosticCollector<JavaFileObject> update(Map<URI, Optional<String>> files) { + List<JavaFileObject> objects = files + .entrySet() + .stream() + .map(e -> findFile(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + + return doUpdate(objects); } /** - * Compile the indicated source file, and its dependencies if they have been modified. + * Exposed for testing only! + */ + public DiagnosticCollector<JavaFileObject> doUpdate(Collection<JavaFileObject> objects) { + List<JCTree.JCCompilationUnit> parsed = objects + .stream() + .map(f -> { + clear(f); + + return f; + }) + .map(this::parse) + .collect(Collectors.toList()); + + // TODO add all dependents + + return compile(parsed); + } + + /** + * File has been deleted + */ + public void delete(URI uri) { + // TODO + } + + private JavaFileObject findFile(URI file, Optional<String> text) { + return text + .map(content -> (JavaFileObject) new StringFileObject(content, file)) + .orElse(fileManager.getRegularFile(new File(file))); + } + + /** + * Parse the indicated source file, and its dependencies if they have been modified. */ public JCTree.JCCompilationUnit parse(JavaFileObject source) { clear(source); @@ -189,28 +419,41 @@ public class JavacHolder { * * If these files reference un-parsed dependencies, those dependencies will also be parsed and compiled. */ - public void compile(Collection<JCTree.JCCompilationUnit> parsed) { - compiler.processAnnotations(compiler.enterTrees(com.sun.tools.javac.util.List.from(parsed))); + public DiagnosticCollector<JavaFileObject> compile(Collection<JCTree.JCCompilationUnit> parsed) { + try { + DiagnosticCollector<JavaFileObject> errors = new DiagnosticCollector<>(); - while (!todo.isEmpty()) { - Env<AttrContext> next = todo.remove(); + onErrorDelegate = error -> { + if (error.getStartPosition() != Diagnostic.NOPOS) + errors.report(error); + }; - try { - // We don't do the desugar or generate phases, because they remove method bodies and methods - Env<AttrContext> attributedTree = compiler.attribute(next); - Queue<Env<AttrContext>> analyzedTree = compiler.flow(attributedTree); - } catch (Exception e) { - LOG.log(Level.SEVERE, "Error compiling " + next.toplevel.sourcefile.getName(), e); + compiler.processAnnotations(compiler.enterTrees(com.sun.tools.javac.util.List.from(parsed))); - // Keep going + while (!todo.isEmpty()) { + Env<AttrContext> next = todo.remove(); + + try { + // We don't do the desugar or generate phases, because they remove method bodies and methods + Env<AttrContext> attributedTree = compiler.attribute(next); + Queue<Env<AttrContext>> analyzedTree = compiler.flow(attributedTree); + } catch (Exception e) { + LOG.log(Level.SEVERE, "Error compiling " + next.toplevel.sourcefile.getName(), e); + + // Keep going + } } + + return errors; + } finally { + onErrorDelegate = error -> {}; } } /** * Clear a file from javac's internal caches */ - public void clear(JavaFileObject source) { + private void clear(JavaFileObject source) { // TODO clear dependencies as well (dependencies should get stored in SymbolIndex) // Forget about this file @@ -271,4 +514,4 @@ public class JavacHolder { throw new RuntimeException(e); } } -} +}
\ No newline at end of file diff --git a/src/main/java/org/javacs/StringFileObject.java b/src/main/java/org/javacs/StringFileObject.java index 582d7b6..0cba9bf 100644 --- a/src/main/java/org/javacs/StringFileObject.java +++ b/src/main/java/org/javacs/StringFileObject.java @@ -2,14 +2,14 @@ package org.javacs; import javax.tools.SimpleJavaFileObject; import java.io.IOException; -import java.nio.file.Path; +import java.net.URI; public class StringFileObject extends SimpleJavaFileObject { public final String content; - public final Path path; + public final URI path; // TODO rename - public StringFileObject(String content, Path path) { - super(path.toUri(), Kind.SOURCE); + public StringFileObject(String content, URI path) { + super(path, Kind.SOURCE); this.content = content; this.path = path; diff --git a/src/main/java/org/javacs/SymbolIndex.java b/src/main/java/org/javacs/SymbolIndex.java index 56bc247..d2fd1c1 100644 --- a/src/main/java/org/javacs/SymbolIndex.java +++ b/src/main/java/org/javacs/SymbolIndex.java @@ -1,23 +1,21 @@ package org.javacs; +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.TreeScanner; +import com.sun.tools.javac.util.Context; +import com.sun.tools.javac.util.Name; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.SymbolInformation; +import org.eclipse.lsp4j.SymbolKind; + +import javax.lang.model.element.ElementKind; +import java.io.IOException; import java.net.URI; import java.util.*; -import java.io.*; -import java.nio.file.*; -import java.util.logging.*; -import java.util.concurrent.*; -import java.util.stream.Collectors; +import java.util.logging.Logger; import java.util.stream.Stream; -import javax.lang.model.element.ElementKind; -import javax.tools.*; - -import javax.tools.JavaFileObject; - -import com.sun.tools.javac.tree.*; -import com.sun.tools.javac.code.*; -import com.sun.tools.javac.util.Context; -import com.sun.tools.javac.util.Name; -import org.eclipse.lsp4j.*; /** * Global index of exported symbol declarations and references. @@ -27,11 +25,6 @@ public class SymbolIndex { private static final Logger LOG = Logger.getLogger("main"); /** - * Completes when initial index is done. Useful for testing. - */ - public final CompletableFuture<Void> initialIndexComplete = new CompletableFuture<>(); - - /** * Contains all symbol declarations and referencs in a single source file */ private static class SourceFileIndex { @@ -39,66 +32,24 @@ public class SymbolIndex { private final EnumMap<ElementKind, Map<String, Set<Location>>> references = new EnumMap<>(ElementKind.class); } + public SymbolIndex(JavacHolder parent) { + this.parent = parent; + } + + /** + * Each index has one compiler as its parent + */ + private final JavacHolder parent; + /** * Source path files, for which we support methods and classes */ private Map<URI, SourceFileIndex> sourcePath = new HashMap<>(); - - public SymbolIndex(Set<Path> classPath, - Set<Path> sourcePath, - Path outputDirectory) { - JavacHolder compiler = new JavacHolder(classPath, sourcePath, outputDirectory); - Indexer indexer = new Indexer(compiler.context); - - // Index exported declarations and references for all files on the source path - // This may take a while, so we'll do it on an extra thread - Thread worker = new Thread("InitialIndex") { - List<JCTree.JCCompilationUnit> parsed = new ArrayList<>(); - List<Path> paths = new ArrayList<>(); - - @Override - public void run() { - // Parse each file - sourcePath.forEach(s -> parseAll(s, parsed, paths)); - - // Compile all parsed files - compiler.compile(parsed); - - parsed.forEach(p -> p.accept(indexer)); - - // TODO minimize memory use during this process - // Instead of doing parse-all / compile-all, - // queue all files, then do parse / compile on each - // If invoked correctly, javac should avoid reparsing the same file twice - // Then, use the same mechanism as the desugar / generate phases to remove method bodies, - // to reclaim memory as we go - - initialIndexComplete.complete(null); - - // TODO verify that compiler and all its resources get destroyed - } - - /** - * Look for .java files and invalidate them - */ - private void parseAll(Path path, List<JCTree.JCCompilationUnit> trees, List<Path> paths) { - if (Files.isDirectory(path)) try { - Files.list(path).forEach(p -> parseAll(p, trees, paths)); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - else if (path.getFileName().toString().endsWith(".java")) { - LOG.info("Index " + path); - JavaFileObject file = compiler.fileManager.getRegularFile(path.toFile()); + public void update(JCTree.JCCompilationUnit compiled) { + Indexer indexer = new Indexer(parent.context); - trees.add(compiler.parse(file)); - paths.add(path); - } - } - }; - - worker.start(); + compiled.accept(indexer); } /** @@ -346,6 +297,41 @@ public class SymbolIndex { } } + /** + * All references to symbol in compilationUnit, including things like local variables that aren't indexe + */ + public static List<Location> nonIndexedReferences(final Symbol symbol, final JCTree.JCCompilationUnit compilationUnit) { + List<Location> result = new ArrayList<>(); + + compilationUnit.accept(new TreeScanner() { + @Override + public void visitSelect(JCTree.JCFieldAccess tree) { + super.visitSelect(tree); + + if (tree.sym != null && tree.sym.equals(symbol)) + result.add(SymbolIndex.location(tree, compilationUnit)); + } + + @Override + public void visitReference(JCTree.JCMemberReference tree) { + super.visitReference(tree); + + if (tree.sym != null && tree.sym.equals(symbol)) + result.add(SymbolIndex.location(tree, compilationUnit)); + } + + @Override + public void visitIdent(JCTree.JCIdent tree) { + super.visitIdent(tree); + + if (tree.sym != null && tree.sym.equals(symbol)) + result.add(SymbolIndex.location(tree, compilationUnit)); + } + }); + + return result; + } + private static int offset(JCTree.JCCompilationUnit compilationUnit, Symbol symbol, int estimate) throws IOException { diff --git a/src/main/java/org/javacs/TreePruner.java b/src/main/java/org/javacs/TreePruner.java new file mode 100644 index 0000000..f9a25a0 --- /dev/null +++ b/src/main/java/org/javacs/TreePruner.java @@ -0,0 +1,59 @@ +package org.javacs; + +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.util.Context; + +import javax.tools.JavaFileObject; +import java.io.IOException; +import java.io.Reader; +import java.net.URI; + +/** + * Fix up the tree to make it easier to autocomplete, index + */ +public class TreePruner { + public final JCTree.JCCompilationUnit tree; + private final Context context; + + public TreePruner(JCTree.JCCompilationUnit tree, Context context) { + this.tree = tree; + this.context = context; + } + + /** + * Remove all statements after the statement the cursor is in + */ + public TreePruner removeStatementsAfterCursor(long cursor) { + tree.accept(new AutocompletePruner(tree.getSourceFile(), cursor, context)); + + return this; + } + + /** + * Insert ';' after the users cursor so we recover from parse errors in a helpful way when doing autocomplete. + */ + public static JavaFileObject putSemicolonAfterCursor(JavaFileObject file, URI path, long cursor) { + try (Reader reader = file.openReader(true)) { + StringBuilder acc = new StringBuilder(); + + for (int i = 0; i < cursor; i++) { + int next = reader.read(); + + if (next == -1) + throw new RuntimeException("End of file " + file + " before cursor " + cursor); + + acc.append((char) next); + } + + acc.append(";"); + + for (int next = reader.read(); next > 0; next = reader.read()) { + acc.append((char) next); + } + + return new StringFileObject(acc.toString(), path); + } catch (IOException e) { + throw ShowMessageException.error("Error reading " + file, e); + } + } +} |