summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Fraser <george@fivetran.com>2017-03-18 22:03:08 -0700
committerGeorge Fraser <george@fivetran.com>2017-03-18 22:03:08 -0700
commit139784693f4bef6078fdc58e7ce17a9416e1a6c8 (patch)
tree73fbcc4035849b836a212f2bf7dcb7ce18318010
parent9b251a1e2ff52e82132427193d062ece48c266b2 (diff)
downloadjava-language-server-139784693f4bef6078fdc58e7ce17a9416e1a6c8.zip
Compiles but structure may not be quite right
-rw-r--r--src/main/java/org/javacs/JavaLanguageServer.java539
-rw-r--r--src/main/java/org/javacs/JavacHolder.java317
-rw-r--r--src/main/java/org/javacs/StringFileObject.java8
-rw-r--r--src/main/java/org/javacs/SymbolIndex.java134
-rw-r--r--src/main/java/org/javacs/TreePruner.java59
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);
+ }
+ }
+}