diff options
Diffstat (limited to 'src')
47 files changed, 1274 insertions, 1223 deletions
diff --git a/src/main/java/org/javacs/CustomLanguageClient.java b/src/main/java/org/javacs/CustomLanguageClient.java deleted file mode 100644 index 3eae781..0000000 --- a/src/main/java/org/javacs/CustomLanguageClient.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.javacs; - -import org.javacs.lsp.*; - -public interface CustomLanguageClient extends LanguageClient { - - @JsonNotification("java/startProgress") - void javaStartProgress(JavaStartProgressParams params); - - @JsonNotification("java/reportProgress") - void javaReportProgress(JavaReportProgressParams params); - - @JsonNotification("java/endProgress") - void javaEndProgress(); -} diff --git a/src/main/java/org/javacs/InferSourcePath.java b/src/main/java/org/javacs/InferSourcePath.java index d54c987..b6f30f2 100644 --- a/src/main/java/org/javacs/InferSourcePath.java +++ b/src/main/java/org/javacs/InferSourcePath.java @@ -45,7 +45,9 @@ class InferSourcePath { var packageName = Objects.toString(Parser.parse(java).getPackageName(), ""); var packagePath = packageName.replace('.', File.separatorChar); var dir = java.getParent(); - if (!dir.endsWith(packagePath)) { + if (packagePath.isEmpty()) { + return Optional.of(dir); + } else if (!dir.endsWith(packagePath)) { LOG.warning("Java source file " + java + " is not in " + packagePath); return Optional.empty(); } else { diff --git a/src/main/java/org/javacs/JavaLanguageServer.java b/src/main/java/org/javacs/JavaLanguageServer.java index cb60c95..dcf2d0e 100644 --- a/src/main/java/org/javacs/JavaLanguageServer.java +++ b/src/main/java/org/javacs/JavaLanguageServer.java @@ -1,34 +1,57 @@ package org.javacs; +import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.sun.source.doctree.DocCommentTree; +import com.sun.source.doctree.DocTree; +import com.sun.source.doctree.ParamTree; +import com.sun.source.tree.MethodTree; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.StringJoiner; -import java.util.concurrent.CompletableFuture; +import java.util.UUID; +import java.util.function.Function; import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; import org.javacs.lsp.*; -class JavaLanguageServer implements LanguageServer { +class JavaLanguageServer extends LanguageServer { private static final Logger LOG = Logger.getLogger("main"); private Path workspaceRoot; - private CustomLanguageClient client; + private final LanguageClient client; private Set<String> externalDependencies = Set.of(); private Set<Path> classPath = Set.of(); JavaCompilerService compiler; - final JavaTextDocumentService textDocuments = new JavaTextDocumentService(this); - final JavaWorkspaceService workspace = new JavaWorkspaceService(this); + private final Map<URI, VersionedContent> activeDocuments = new HashMap<>(); - private static DiagnosticSeverity severity(Diagnostic.Kind kind) { + private static int severity(Diagnostic.Kind kind) { switch (kind) { case ERROR: return DiagnosticSeverity.Error; @@ -55,7 +78,7 @@ class JavaLanguageServer implements LanguageServer { void publishDiagnostics(Collection<URI> files, List<Diagnostic<? extends JavaFileObject>> javaDiagnostics) { for (var f : files) { - List<org.eclipse.lsp4j.Diagnostic> ds = new ArrayList<>(); + List<org.javacs.lsp.Diagnostic> ds = new ArrayList<>(); for (var j : javaDiagnostics) { if (j.getSource() == null) { LOG.warning("No source in warning " + j.getMessage(null)); @@ -64,19 +87,18 @@ class JavaLanguageServer implements LanguageServer { var uri = j.getSource().toUri(); if (uri.equals(f)) { - var content = textDocuments.contents(uri).content; + var content = contents(uri).content; var start = position(content, j.getStartPosition()); var end = position(content, j.getEndPosition()); - var sev = severity(j.getKind()); - var d = new org.eclipse.lsp4j.Diagnostic(); - d.setSeverity(sev); - d.setRange(new Range(start, end)); - d.setCode(j.getCode()); - d.setMessage(j.getMessage(null)); + var d = new org.javacs.lsp.Diagnostic(); + d.severity = severity(j.getKind()); + d.range = new Range(start, end); + d.code = j.getCode(); + d.message = j.getMessage(null); ds.add(d); } } - client.publishDiagnostics(new PublishDiagnosticsParams(f.toString(), ds)); + client.publishDiagnostics(new PublishDiagnosticsParams(f, ds)); } } @@ -91,30 +113,44 @@ class JavaLanguageServer implements LanguageServer { publishDiagnostics(uris, compiler.compileBatch(uris).lint()); } + private static final Gson gson = new Gson(); + + private void javaStartProgress(JavaStartProgressParams params) { + client.customNotification("java/startProgress", gson.toJsonTree(params)); + } + + private void javaReportProgress(JavaReportProgressParams params) { + client.customNotification("java/reportProgress", gson.toJsonTree(params)); + } + + private void javaEndProgress() { + client.customNotification("java/endProgress", JsonNull.INSTANCE); + } + private JavaCompilerService createCompiler() { Objects.requireNonNull(workspaceRoot, "Can't create compiler because workspaceRoot has not been initialized"); - client.javaStartProgress(new JavaStartProgressParams("Configure javac")); - client.javaReportProgress(new JavaReportProgressParams("Finding source roots")); + javaStartProgress(new JavaStartProgressParams("Configure javac")); + javaReportProgress(new JavaReportProgressParams("Finding source roots")); var sourcePath = InferSourcePath.sourcePath(workspaceRoot); // TODO show each folder as we find it // If classpath is specified by the user, don't infer anything if (!classPath.isEmpty()) { - client.javaEndProgress(); + javaEndProgress(); return new JavaCompilerService(sourcePath, classPath, Collections.emptySet()); } // Otherwise, combine inference with user-specified external dependencies else { var infer = new InferConfig(workspaceRoot, externalDependencies); - client.javaReportProgress(new JavaReportProgressParams("Inferring class path")); + javaReportProgress(new JavaReportProgressParams("Inferring class path")); var classPath = infer.classPath(); - client.javaReportProgress(new JavaReportProgressParams("Inferring doc path")); + javaReportProgress(new JavaReportProgressParams("Inferring doc path")); var docPath = infer.buildDocPath(); - client.javaEndProgress(); + javaEndProgress(); return new JavaCompilerService(sourcePath, classPath, docPath); } } @@ -134,56 +170,818 @@ class JavaLanguageServer implements LanguageServer { } @Override - public CompletableFuture<InitializeResult> initialize(InitializeParams params) { - this.workspaceRoot = Paths.get(URI.create(params.getRootUri())); + public InitializeResult initialize(InitializeParams params) { + this.workspaceRoot = Paths.get(params.rootUri); + + var c = new JsonObject(); + c.addProperty("textDocumentSync", 2); // Incremental + c.addProperty("hoverProvider", true); + var completionOptions = new JsonObject(); + completionOptions.addProperty("resolveProvider", true); + var triggerCharacters = new JsonArray(); + triggerCharacters.add("."); + completionOptions.add("triggerCharacters", triggerCharacters); + c.add("completionProvider", completionOptions); + var signatureHelpOptions = new JsonObject(); + var signatureTrigger = new JsonArray(); + signatureTrigger.add("("); + signatureTrigger.add(","); + signatureHelpOptions.add("triggerCharacters", signatureTrigger); + c.add("signatureHelpProvider", signatureHelpOptions); + c.addProperty("referencesProvider", true); + c.addProperty("definitionProvider", true); + c.addProperty("workspaceSymbolProvider", true); + c.addProperty("documentSymbolProvider", true); + c.addProperty("documentFormattingProvider", true); + var codeLensOptions = new JsonObject(); + codeLensOptions.addProperty("resolveProvider", true); + c.add("codeLensProvider", codeLensOptions); - var result = new InitializeResult(); - var c = new ServerCapabilities(); + return new InitializeResult(c); + } + + @Override + public void initialized() { + this.compiler = createCompiler(); + } - c.setTextDocumentSync(TextDocumentSyncKind.Incremental); - c.setDefinitionProvider(true); - c.setCompletionProvider(new CompletionOptions(true, List.of("."))); - c.setHoverProvider(true); - c.setWorkspaceSymbolProvider(true); - c.setReferencesProvider(true); - c.setDocumentSymbolProvider(true); - c.setSignatureHelpProvider(new SignatureHelpOptions(List.of("(", ","))); - c.setDocumentFormattingProvider(true); - c.setCodeLensProvider(new CodeLensOptions(true)); + @Override + public void shutdown() {} - result.setCapabilities(c); + public JavaLanguageServer(LanguageClient client) { + this.client = client; + } - return CompletableFuture.completedFuture(result); + @Override + public List<SymbolInformation> workspaceSymbols(WorkspaceSymbolParams params) { + List<SymbolInformation> list = + compiler.findSymbols(params.query) + .map(Parser::asSymbolInformation) + .limit(50) + .collect(Collectors.toList()); + return list; } @Override - public void initialized(InitializedParams params) { - this.compiler = createCompiler(); + public void didChangeConfiguration(DidChangeConfigurationParams change) { + var settings = (JsonObject) change.settings; + var java = settings.getAsJsonObject("java"); + + var externalDependencies = java.getAsJsonArray("externalDependencies"); + var strings = new HashSet<String>(); + for (var each : externalDependencies) strings.add(each.getAsString()); + setExternalDependencies(strings); + + var classPath = java.getAsJsonArray("classPath"); + var paths = new HashSet<Path>(); + for (var each : classPath) paths.add(Paths.get(each.getAsString()).toAbsolutePath()); + setClassPath(paths); + } + + @Override + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {} + + private Integer completionItemKind(Element e) { + switch (e.getKind()) { + case ANNOTATION_TYPE: + return CompletionItemKind.Interface; + case CLASS: + return CompletionItemKind.Class; + case CONSTRUCTOR: + return CompletionItemKind.Constructor; + case ENUM: + return CompletionItemKind.Enum; + case ENUM_CONSTANT: + return CompletionItemKind.EnumMember; + case EXCEPTION_PARAMETER: + return CompletionItemKind.Variable; + case FIELD: + return CompletionItemKind.Field; + case STATIC_INIT: + case INSTANCE_INIT: + return CompletionItemKind.Function; + case INTERFACE: + return CompletionItemKind.Interface; + case LOCAL_VARIABLE: + return CompletionItemKind.Variable; + case METHOD: + return CompletionItemKind.Method; + case PACKAGE: + return CompletionItemKind.Module; + case PARAMETER: + return CompletionItemKind.Variable; + case RESOURCE_VARIABLE: + return CompletionItemKind.Variable; + case TYPE_PARAMETER: + return CompletionItemKind.TypeParameter; + case OTHER: + default: + return null; + } } + /** Cache of completions from the last call to `completion` */ + private final Map<String, Completion> lastCompletions = new HashMap<>(); + @Override - public CompletableFuture<Object> shutdown() { - return CompletableFuture.completedFuture(null); + public Optional<CompletionList> completion(TextDocumentPositionParams position) { + var uri = position.textDocument.uri; + var content = contents(uri).content; + var line = position.position.line + 1; + var column = position.position.character + 1; + lastCompletions.clear(); + // Figure out what kind of completion we want to do + var maybeCtx = compiler.parseFile(uri, content).completionContext(line, column); + if (!maybeCtx.isPresent()) { + var items = new ArrayList<CompletionItem>(); + for (var name : CompileFocus.TOP_LEVEL_KEYWORDS) { + var i = new CompletionItem(); + i.label = name; + i.kind = CompletionItemKind.Keyword; + i.detail = "keyword"; + items.add(i); + } + return Optional.of(new CompletionList(true, items)); + } + // Compile again, focusing on a region that depends on what type of completion we want to do + var ctx = maybeCtx.get(); + var focus = compiler.compileFocus(uri, content, ctx.line, ctx.character); + // Do a specific type of completion + List<Completion> cs; + boolean isIncomplete; + switch (ctx.kind) { + case MemberSelect: + cs = focus.completeMembers(false); + isIncomplete = false; + break; + case MemberReference: + cs = focus.completeMembers(true); + isIncomplete = false; + break; + case Identifier: + cs = focus.completeIdentifiers(ctx.inClass, ctx.inMethod, ctx.partialName); + isIncomplete = cs.size() >= CompileFocus.MAX_COMPLETION_ITEMS; + break; + case Annotation: + cs = focus.completeAnnotations(ctx.partialName); + isIncomplete = cs.size() >= CompileFocus.MAX_COMPLETION_ITEMS; + break; + case Case: + cs = focus.completeCases(); + isIncomplete = false; + break; + default: + throw new RuntimeException("Unexpected completion context " + ctx.kind); + } + // Convert to CompletionItem + var result = new ArrayList<CompletionItem>(); + for (var c : cs) { + var i = new CompletionItem(); + var id = UUID.randomUUID().toString(); + i.data = new JsonPrimitive(id); + lastCompletions.put(id, c); + if (c.element != null) { + i.label = c.element.getSimpleName().toString(); + i.kind = completionItemKind(c.element); + // Detailed name will be resolved later, using docs to fill in method names + if (!(c.element instanceof ExecutableElement)) i.detail = c.element.toString(); + i.sortText = 2 + i.label; + } else if (c.packagePart != null) { + i.label = c.packagePart.name; + i.kind = CompletionItemKind.Module; + i.detail = c.packagePart.fullName; + i.sortText = 2 + i.label; + } else if (c.keyword != null) { + i.label = c.keyword; + i.kind = CompletionItemKind.Keyword; + i.detail = "keyword"; + i.sortText = 3 + i.label; + } else if (c.className != null) { + i.label = Parser.lastName(c.className.name); + i.kind = CompletionItemKind.Class; + i.detail = c.className.name; + if (c.className.isImported) i.sortText = 2 + i.label; + else i.sortText = 4 + i.label; + } else if (c.snippet != null) { + i.label = c.snippet.label; + i.kind = CompletionItemKind.Snippet; + i.insertText = c.snippet.snippet; + i.insertTextFormat = InsertTextFormat.Snippet; + i.sortText = 1 + i.label; + } else throw new RuntimeException(c + " is not valid"); + + result.add(i); + } + return Optional.of(new CompletionList(isIncomplete, result)); + } + + private String resolveDocDetail(MethodTree doc) { + var args = new StringJoiner(", "); + for (var p : doc.getParameters()) { + args.add(p.getName()); + } + return String.format("%s %s(%s)", doc.getReturnType(), doc.getName(), args); + } + + private String resolveDefaultDetail(ExecutableElement method) { + var args = new StringJoiner(", "); + var missingParamNames = + method.getParameters().stream().allMatch(p -> p.getSimpleName().toString().matches("arg\\d+")); + for (var p : method.getParameters()) { + if (missingParamNames) args.add(ShortTypePrinter.print(p.asType())); + else args.add(p.getSimpleName().toString()); + } + return String.format("%s %s(%s)", ShortTypePrinter.print(method.getReturnType()), method.getSimpleName(), args); + } + + private String asMarkdown(List<? extends DocTree> lines) { + var join = new StringJoiner("\n"); + for (var l : lines) join.add(l.toString()); + var html = join.toString(); + return Docs.htmlToMarkdown(html); + } + + private String asMarkdown(DocCommentTree comment) { + var lines = comment.getFirstSentence(); + return asMarkdown(lines); + } + + private MarkupContent asMarkupContent(DocCommentTree comment) { + var markdown = asMarkdown(comment); + var content = new MarkupContent(); + content.kind = MarkupKind.Markdown; + content.value = markdown; + return content; } @Override - public void exit() {} + public CompletionItem resolveCompletionItem(CompletionItem unresolved) { + var idJson = (JsonPrimitive) unresolved.data; + var id = idJson.getAsString(); + var cached = lastCompletions.get(id); + if (cached == null) { + LOG.warning("CompletionItem " + id + " was not in the cache"); + return unresolved; + } + if (cached.element != null) { + if (cached.element instanceof ExecutableElement) { + var method = (ExecutableElement) cached.element; + var tree = compiler.docs().methodTree(method); + var detail = tree.map(this::resolveDocDetail).orElse(resolveDefaultDetail(method)); + unresolved.detail = detail; + + var doc = compiler.docs().methodDoc(method); + var markdown = doc.map(this::asMarkupContent); + if (markdown.isPresent()) unresolved.documentation = markdown.get(); + } else if (cached.element instanceof TypeElement) { + var type = (TypeElement) cached.element; + var doc = compiler.docs().classDoc(type); + var markdown = doc.map(this::asMarkupContent); + if (markdown.isPresent()) unresolved.documentation = markdown.get(); + } else { + LOG.info("Don't know how to look up docs for element " + cached.element); + } + // TODO constructors, fields + } else if (cached.className != null) { + var doc = compiler.docs().classDoc(cached.className.name); + var markdown = doc.map(this::asMarkupContent); + if (markdown.isPresent()) unresolved.documentation = markdown.get(); + } + return unresolved; + } + + private String hoverTypeDeclaration(TypeElement t) { + var result = new StringBuilder(); + switch (t.getKind()) { + case ANNOTATION_TYPE: + result.append("@interface"); + break; + case INTERFACE: + result.append("interface"); + break; + case CLASS: + result.append("class"); + break; + case ENUM: + result.append("enum"); + break; + default: + LOG.warning("Don't know what to call type element " + t); + result.append("???"); + } + result.append(" ").append(ShortTypePrinter.print(t.asType())); + var superType = ShortTypePrinter.print(t.getSuperclass()); + switch (superType) { + case "Object": + case "none": + break; + default: + result.append(" extends ").append(superType); + } + return result.toString(); + } + + private String hoverCode(Element e) { + if (e instanceof ExecutableElement) { + var m = (ExecutableElement) e; + return ShortTypePrinter.printMethod(m); + } else if (e instanceof VariableElement) { + var v = (VariableElement) e; + return ShortTypePrinter.print(v.asType()) + " " + v; + } else if (e instanceof TypeElement) { + var t = (TypeElement) e; + var lines = new StringJoiner("\n"); + lines.add(hoverTypeDeclaration(t) + " {"); + for (var member : t.getEnclosedElements()) { + // TODO check accessibility + if (member instanceof ExecutableElement || member instanceof VariableElement) { + lines.add(" " + hoverCode(member) + ";"); + } else if (member instanceof TypeElement) { + lines.add(" " + hoverTypeDeclaration((TypeElement) member) + " { /* removed */ }"); + } + } + lines.add("}"); + return lines.toString(); + } else return e.toString(); + } + + private Optional<String> hoverDocs(Element e) { + if (e instanceof ExecutableElement) { + var m = (ExecutableElement) e; + return compiler.docs().methodDoc(m).map(this::asMarkdown); + } else if (e instanceof TypeElement) { + var t = (TypeElement) e; + return compiler.docs().classDoc(t).map(this::asMarkdown); + } else return Optional.empty(); + } + + private CompileFile hoverCache; + + private void updateHoverCache(URI uri, String contents) { + if (hoverCache == null || !hoverCache.file.equals(uri) || !hoverCache.contents.equals(contents)) { + LOG.info("File has changed since last hover, recompiling"); + hoverCache = compiler.compileFile(uri, contents); + } + } @Override - public TextDocumentService getTextDocumentService() { - return textDocuments; + public Optional<Hover> hover(TextDocumentPositionParams position) { + // Compile entire file if it's changed since last hover + var uri = position.textDocument.uri; + var content = contents(uri).content; + updateHoverCache(uri, content); + + // Find element undeer cursor + var line = position.position.line + 1; + var column = position.position.character + 1; + var el = hoverCache.element(line, column); + if (!el.isPresent()) return Optional.empty(); + + // Add code hover message + var result = new ArrayList<MarkedString>(); + var code = hoverCode(el.get()); + result.add(new MarkedString("java.hover", code)); + // Add docs hover message + var docs = hoverDocs(el.get()); + if (docs.isPresent()) { + result.add(new MarkedString("markdown", docs.get())); + } + + return Optional.of(new Hover(result)); + } + + private List<ParameterInformation> signatureParamsFromDocs(MethodTree method, DocCommentTree doc) { + var ps = new ArrayList<ParameterInformation>(); + var paramComments = new HashMap<String, String>(); + for (var tag : doc.getBlockTags()) { + if (tag.getKind() == DocTree.Kind.PARAM) { + var param = (ParamTree) tag; + paramComments.put(param.getName().toString(), asMarkdown(param.getDescription())); + } + } + for (var param : method.getParameters()) { + var info = new ParameterInformation(); + var name = param.getName().toString(); + info.label = name; + if (paramComments.containsKey(name)) { + var markdown = paramComments.get(name); + info.documentation = new MarkupContent("markdown", markdown); + } else { + var markdown = Objects.toString(param.getType(), ""); + info.documentation = new MarkupContent("markdown", markdown); + } + ps.add(info); + } + return ps; + } + + private List<ParameterInformation> signatureParamsFromMethod(ExecutableElement e) { + var missingParamNames = ShortTypePrinter.missingParamNames(e); + var ps = new ArrayList<ParameterInformation>(); + for (var v : e.getParameters()) { + var p = new ParameterInformation(); + if (missingParamNames) p.label = ShortTypePrinter.print(v.asType()); + else p.label = v.getSimpleName().toString(); + ps.add(p); + } + return ps; + } + + private SignatureInformation asSignatureInformation(ExecutableElement e) { + var i = new SignatureInformation(); + var ps = signatureParamsFromMethod(e); + var doc = compiler.docs().methodDoc(e); + var tree = compiler.docs().methodTree(e); + if (doc.isPresent() && tree.isPresent()) ps = signatureParamsFromDocs(tree.get(), doc.get()); + var args = ps.stream().map(p -> p.label).collect(Collectors.joining(", ")); + var name = e.getSimpleName().toString(); + if (name.equals("<init>")) name = e.getEnclosingElement().getSimpleName().toString(); + i.label = name + "(" + args + ")"; + i.parameters = ps; + return i; + } + + private SignatureHelp asSignatureHelp(MethodInvocation invoke) { + // TODO use docs to get parameter names + var sigs = new ArrayList<SignatureInformation>(); + for (var e : invoke.overloads) { + sigs.add(asSignatureInformation(e)); + } + var activeSig = invoke.activeMethod.map(invoke.overloads::indexOf).orElse(0); + return new SignatureHelp(sigs, activeSig, invoke.activeParameter); } @Override - public WorkspaceService getWorkspaceService() { - return workspace; + public Optional<SignatureHelp> signatureHelp(TextDocumentPositionParams position) { + var uri = position.textDocument.uri; + var content = contents(uri).content; + var line = position.position.line + 1; + var column = position.position.character + 1; + var focus = compiler.compileFocus(uri, content, line, column); + var help = focus.methodInvocation().map(this::asSignatureHelp); + return help; } - void installClient(CustomLanguageClient client) { - this.client = client; + @Override + public List<Location> gotoDefinition(TextDocumentPositionParams position) { + var fromUri = position.textDocument.uri; + var fromLine = position.position.line + 1; + var fromColumn = position.position.character + 1; + var fromContent = contents(fromUri).content; + var fromFocus = compiler.compileFocus(fromUri, fromContent, fromLine, fromColumn); + var toEl = fromFocus.element(); + var toUri = fromFocus.declaringFile(toEl); + if (!toUri.isPresent()) return List.of(); + var toContent = contents(toUri.get()).content; + var toFile = compiler.compileFile(toUri.get(), toContent); + var toPath = toFile.find(new Ptr(toEl)); + if (!toPath.isPresent()) return List.of(); + // Figure out where in the file the definition is + var toRange = toFile.range(toPath.get()); + if (!toRange.isPresent()) return List.of(); + var to = new Location(toUri.get(), toRange.get()); + return List.of(to); } - CustomLanguageClient client() { - return this.client; + class ReportProgress implements ReportReferencesProgress, AutoCloseable { + private final Function<Integer, String> scanMessage, checkMessage; + + ReportProgress( + String startMessage, Function<Integer, String> scanMessage, Function<Integer, String> checkMessage) { + this.scanMessage = scanMessage; + this.checkMessage = checkMessage; + javaStartProgress(new JavaStartProgressParams(startMessage)); + } + + private int percent(int n, int d) { + double nD = n, dD = d; + double ratio = nD / dD; + return (int) (ratio * 100); + } + + public void scanForPotentialReferences(int nScanned, int nFiles) { + var message = scanMessage.apply(nFiles); + if (nScanned == 0) { + javaReportProgress(new JavaReportProgressParams(message)); + } else { + var increment = percent(nScanned, nFiles) > percent(nScanned - 1, nFiles) ? 1 : 0; + javaReportProgress(new JavaReportProgressParams(message, increment)); + } + } + + public void checkPotentialReferences(int nCompiled, int nPotential) { + var message = checkMessage.apply(nCompiled); + if (nCompiled == 0) { + javaReportProgress(new JavaReportProgressParams(message)); + } else { + var increment = percent(nCompiled, nPotential) > percent(nCompiled - 1, nPotential) ? 1 : 0; + javaReportProgress(new JavaReportProgressParams(message, increment)); + } + } + + @Override + public void close() { + javaEndProgress(); + } + } + + @Override + public List<Location> findReferences(ReferenceParams position) { + var toUri = position.textDocument.uri; + var toContent = contents(toUri).content; + var toLine = position.position.line + 1; + var toColumn = position.position.character + 1; + var toEl = compiler.compileFocus(toUri, toContent, toLine, toColumn).element(); + var fromFiles = compiler.potentialReferences(toEl); + if (fromFiles.isEmpty()) return List.of(); + var batch = compiler.compileBatch(fromFiles); + var fromTreePaths = batch.references(toEl); + var result = new ArrayList<Location>(); + for (var path : fromTreePaths) { + var fromUri = path.getCompilationUnit().getSourceFile().toUri(); + var fromRange = batch.range(path); + if (!fromRange.isPresent()) { + LOG.warning(String.format("Couldn't locate `%s`", path.getLeaf())); + continue; + } + var from = new Location(fromUri, fromRange.get()); + result.add(from); + } + return result; + } + + @Override + public List<SymbolInformation> documentSymbol(DocumentSymbolParams params) { + var uri = params.textDocument.uri; + var content = contents(uri).content; + var result = + Parser.documentSymbols(Paths.get(uri), content) + .stream() + .map(Parser::asSymbolInformation) + .collect(Collectors.toList()); + return result; + } + + @Override + public List<CodeLens> codeLens(CodeLensParams params) { + // TODO just create a blank code lens on every method, then resolve it async + var uri = params.textDocument.uri; + var content = contents(uri).content; + var parse = compiler.parseFile(uri, content); + var declarations = parse.declarations(); + var result = new ArrayList<CodeLens>(); + for (var d : declarations) { + var range = parse.range(d); + if (!range.isPresent()) continue; + var className = JavaCompilerService.className(d); + var memberName = JavaCompilerService.memberName(d); + // If test class or method, add "Run Test" code lens + if (parse.isTestClass(d)) { + var arguments = new JsonArray(); + arguments.add(uri.toString()); + arguments.add(className); + arguments.add(JsonNull.INSTANCE); + var command = new Command("Run All Tests", "java.command.test.run", arguments); + var lens = new CodeLens(range.get(), command, null); + result.add(lens); + // TODO run all tests in file + // TODO run all tests in package + } else if (parse.isTestMethod(d)) { + var arguments = new JsonArray(); + arguments.add(uri.toString()); + arguments.add(className); + if (memberName.isPresent()) arguments.add(memberName.get()); + else arguments.add(JsonNull.INSTANCE); + var command = new Command("Run Test", "java.command.test.run", arguments); + var lens = new CodeLens(range.get(), command, null); + result.add(lens); + } + // If method or field, add an unresolved "_ references" code lens + if (memberName.isPresent()) { + var start = range.get().start; + var line = start.line; + var character = start.character; + var data = new JsonArray(); + data.add("java.command.findReferences"); + data.add(uri.toString()); + data.add(line); + data.add(character); + data.add(new Ptr(d).toString()); + var lens = new CodeLens(range.get(), null, data); + result.add(lens); + } + } + return result; + } + + private Map<Ptr, Integer> cacheCountReferences = Collections.emptyMap(); + private URI cacheCountReferencesFile = URI.create("file:///NONE"); + + private void updateCacheCountReferences(URI current) { + if (cacheCountReferencesFile.equals(current)) return; + LOG.info(String.format("Update cached reference count to %s...", current)); + var content = contents(current).content; + cacheCountReferences = compiler.countReferences(current, content); + cacheCountReferencesFile = current; + } + + @Override + public CodeLens resolveCodeLens(CodeLens unresolved) { + // Unpack data + var data = unresolved.data; + var command = data.get(0).getAsString(); + assert command.equals("java.command.findReferences"); + var uriString = data.get(1).getAsString(); + var line = data.get(2).getAsInt(); + var character = data.get(3).getAsInt(); + var ptrString = data.get(4).getAsString(); + // Parse data + var uri = URI.create(uriString); + var ptr = new Ptr(ptrString); + // Update cache if necessary + updateCacheCountReferences(uri); + // Read reference count from cache + var count = cacheCountReferences.getOrDefault(ptr, 0); + // Update command + String title; + if (count == 1) title = "1 reference"; + else title = String.format("%d references", count); + var arguments = new JsonArray(); + arguments.add(uri.toString()); + arguments.add(line); + arguments.add(character); + unresolved.command = new Command(title, command, arguments); + + return unresolved; + } + + private List<TextEdit> fixImports(URI java) { + var contents = contents(java).content; + var fix = compiler.compileFile(java, contents).fixImports(); + // TODO if imports already match fixed-imports, return empty list + // TODO preserve comments and other details of existing imports + var edits = new ArrayList<TextEdit>(); + // Delete all existing imports + for (var i : fix.parsed.getImports()) { + if (!i.isStatic()) { + var offset = fix.sourcePositions.getStartPosition(fix.parsed, i); + var line = (int) fix.parsed.getLineMap().getLineNumber(offset) - 1; + var delete = new TextEdit(new Range(new Position(line, 0), new Position(line + 1, 0)), ""); + edits.add(delete); + } + } + if (fix.fixedImports.isEmpty()) return edits; + // Find a place to insert the new imports + long insertLine = -1; + var insertText = new StringBuilder(); + // If there are imports, use the start of the first import as the insert position + for (var i : fix.parsed.getImports()) { + if (!i.isStatic() && insertLine == -1) { + long offset = fix.sourcePositions.getStartPosition(fix.parsed, i); + insertLine = fix.parsed.getLineMap().getLineNumber(offset) - 1; + } + } + // If there are no imports, insert after the package declaration + if (insertLine == -1 && fix.parsed.getPackageName() != null) { + long offset = fix.sourcePositions.getEndPosition(fix.parsed, fix.parsed.getPackageName()); + insertLine = fix.parsed.getLineMap().getLineNumber(offset); + insertText.append("\n"); + } + // If there are no imports and no package, insert at the top of the file + if (insertLine == -1) { + insertLine = 0; + } + // Insert each import + fix.fixedImports + .stream() + .sorted() + .forEach( + i -> { + insertText.append("import ").append(i).append(";\n"); + }); + var insertPosition = new Position((int) insertLine, 0); + var insert = new TextEdit(new Range(insertPosition, insertPosition), insertText.toString()); + edits.add(insert); + return edits; + } + + @Override + public List<TextEdit> formatting(DocumentFormattingParams params) { + var uri = params.textDocument.uri; + return fixImports(uri); + } + + @Override + public WorkspaceEdit rename(RenameParams params) { + return null; // TODO + } + + private boolean isJava(URI uri) { + return uri.getPath().endsWith(".java"); + } + + @Override + public void didOpenTextDocument(DidOpenTextDocumentParams params) { + var document = params.textDocument; + var uri = document.uri; + if (isJava(uri)) { + activeDocuments.put(uri, new VersionedContent(document.text, document.version)); + lint(Collections.singleton(uri)); + } + } + + @Override + public void didChangeTextDocument(DidChangeTextDocumentParams params) { + var document = params.textDocument; + var uri = document.uri; + if (isJava(uri)) { + var existing = activeDocuments.get(uri); + var newText = existing.content; + + if (document.version > existing.version) { + for (var change : params.contentChanges) { + if (change.range == null) newText = change.text; + else newText = patch(newText, change); + } + + activeDocuments.put(uri, new VersionedContent(newText, document.version)); + } else LOG.warning("Ignored change with version " + document.version + " <= " + existing.version); + } + } + + private String patch(String sourceText, TextDocumentContentChangeEvent change) { + try { + var range = change.range; + var reader = new BufferedReader(new StringReader(sourceText)); + var writer = new StringWriter(); + + // Skip unchanged lines + int line = 0; + + while (line < range.start.line) { + writer.write(reader.readLine() + '\n'); + line++; + } + + // Skip unchanged chars + for (int character = 0; character < range.start.character; character++) writer.write(reader.read()); + + // Write replacement text + writer.write(change.text); + + // Skip replaced text + reader.skip(change.rangeLength); + + // Write remaining text + while (true) { + int next = reader.read(); + + if (next == -1) return writer.toString(); + else writer.write(next); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void didCloseTextDocument(DidCloseTextDocumentParams params) { + var document = params.textDocument; + var uri = document.uri; + if (isJava(uri)) { + // Remove from source cache + activeDocuments.remove(uri); + + // Clear diagnostics + publishDiagnostics(Collections.singletonList(uri), List.of()); + } + } + + @Override + public void didSaveTextDocument(DidSaveTextDocumentParams params) { + var uri = params.textDocument.uri; + if (isJava(uri)) { + // Re-lint all active documents + lint(activeDocuments.keySet()); + // TODO update config when java file implies a new source root + } + // TODO update config when pom.xml changes + } + + Set<URI> activeDocuments() { + return activeDocuments.keySet(); + } + + VersionedContent contents(URI openFile) { + if (activeDocuments.containsKey(openFile)) { + return activeDocuments.get(openFile); + } else { + try { + var content = Files.readAllLines(Paths.get(openFile)).stream().collect(Collectors.joining("\n")); + return new VersionedContent(content, -1); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } } diff --git a/src/main/java/org/javacs/JavaTextDocumentService.java b/src/main/java/org/javacs/JavaTextDocumentService.java deleted file mode 100644 index 6d7dd30..0000000 --- a/src/main/java/org/javacs/JavaTextDocumentService.java +++ /dev/null @@ -1,787 +0,0 @@ -package org.javacs; - -import com.google.gson.JsonPrimitive; -import com.sun.source.doctree.DocCommentTree; -import com.sun.source.doctree.DocTree; -import com.sun.source.doctree.ParamTree; -import com.sun.source.tree.MethodTree; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; -import java.lang.annotation.Annotation; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.function.Function; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import javax.lang.model.element.*; -import org.javacs.lsp.*; - -class JavaTextDocumentService implements TextDocumentService { - private final JavaLanguageServer server; - private final Map<URI, VersionedContent> activeDocuments = new HashMap<>(); - - JavaTextDocumentService(JavaLanguageServer server) { - this.server = server; - } - - private CompletionItemKind completionItemKind(Element e) { - switch (e.getKind()) { - case ANNOTATION_TYPE: - return CompletionItemKind.Interface; - case CLASS: - return CompletionItemKind.Class; - case CONSTRUCTOR: - return CompletionItemKind.Constructor; - case ENUM: - return CompletionItemKind.Enum; - case ENUM_CONSTANT: - return CompletionItemKind.EnumMember; - case EXCEPTION_PARAMETER: - return CompletionItemKind.Variable; - case FIELD: - return CompletionItemKind.Field; - case STATIC_INIT: - case INSTANCE_INIT: - return CompletionItemKind.Function; - case INTERFACE: - return CompletionItemKind.Interface; - case LOCAL_VARIABLE: - return CompletionItemKind.Variable; - case METHOD: - return CompletionItemKind.Method; - case PACKAGE: - return CompletionItemKind.Module; - case PARAMETER: - return CompletionItemKind.Variable; - case RESOURCE_VARIABLE: - return CompletionItemKind.Variable; - case TYPE_PARAMETER: - return CompletionItemKind.TypeParameter; - case OTHER: - default: - return null; - } - } - - /** Cache of completions from the last call to `completion` */ - private final Map<String, Completion> lastCompletions = new HashMap<>(); - - @Override - public CompletableFuture<Either<List<CompletionItem>, CompletionList>> completion(CompletionParams position) { - var uri = URI.create(position.getTextDocument().getUri()); - var content = contents(uri).content; - var line = position.getPosition().getLine() + 1; - var column = position.getPosition().getCharacter() + 1; - lastCompletions.clear(); - // Figure out what kind of completion we want to do - var maybeCtx = server.compiler.parseFile(uri, content).completionContext(line, column); - if (!maybeCtx.isPresent()) { - var items = new ArrayList<CompletionItem>(); - for (var name : CompileFocus.TOP_LEVEL_KEYWORDS) { - var i = new CompletionItem(); - i.setLabel(name); - i.setKind(CompletionItemKind.Keyword); - i.setDetail("keyword"); - items.add(i); - } - var list = new CompletionList(true, items); - return CompletableFuture.completedFuture(Either.forRight(list)); - } - // Compile again, focusing on a region that depends on what type of completion we want to do - var ctx = maybeCtx.get(); - var focus = server.compiler.compileFocus(uri, content, ctx.line, ctx.character); - // Do a specific type of completion - List<Completion> cs; - boolean isIncomplete; - switch (ctx.kind) { - case MemberSelect: - cs = focus.completeMembers(false); - isIncomplete = false; - break; - case MemberReference: - cs = focus.completeMembers(true); - isIncomplete = false; - break; - case Identifier: - cs = focus.completeIdentifiers(ctx.inClass, ctx.inMethod, ctx.partialName); - isIncomplete = cs.size() >= CompileFocus.MAX_COMPLETION_ITEMS; - break; - case Annotation: - cs = focus.completeAnnotations(ctx.partialName); - isIncomplete = cs.size() >= CompileFocus.MAX_COMPLETION_ITEMS; - break; - case Case: - cs = focus.completeCases(); - isIncomplete = false; - break; - default: - throw new RuntimeException("Unexpected completion context " + ctx.kind); - } - // Convert to CompletionItem - var result = new ArrayList<CompletionItem>(); - for (var c : cs) { - var i = new CompletionItem(); - var id = UUID.randomUUID().toString(); - i.setData(id); - lastCompletions.put(id, c); - if (c.element != null) { - i.setLabel(c.element.getSimpleName().toString()); - i.setKind(completionItemKind(c.element)); - // Detailed name will be resolved later, using docs to fill in method names - if (!(c.element instanceof ExecutableElement)) i.setDetail(c.element.toString()); - i.setSortText(2 + i.getLabel()); - } else if (c.packagePart != null) { - i.setLabel(c.packagePart.name); - i.setKind(CompletionItemKind.Module); - i.setDetail(c.packagePart.fullName); - i.setSortText(2 + i.getLabel()); - } else if (c.keyword != null) { - i.setLabel(c.keyword); - i.setKind(CompletionItemKind.Keyword); - i.setDetail("keyword"); - i.setSortText(3 + i.getLabel()); - } else if (c.className != null) { - i.setLabel(Parser.lastName(c.className.name)); - i.setKind(CompletionItemKind.Class); - i.setDetail(c.className.name); - if (c.className.isImported) i.setSortText(2 + i.getLabel()); - else i.setSortText(4 + i.getLabel()); - } else if (c.snippet != null) { - i.setLabel(c.snippet.label); - i.setKind(CompletionItemKind.Snippet); - i.setInsertText(c.snippet.snippet); - i.setInsertTextFormat(InsertTextFormat.Snippet); - i.setSortText(1 + i.getLabel()); - } else throw new RuntimeException(c + " is not valid"); - - result.add(i); - } - return CompletableFuture.completedFuture(Either.forRight(new CompletionList(isIncomplete, result))); - } - - private String resolveDocDetail(MethodTree doc) { - var args = new StringJoiner(", "); - for (var p : doc.getParameters()) { - args.add(p.getName()); - } - return String.format("%s %s(%s)", doc.getReturnType(), doc.getName(), args); - } - - private String resolveDefaultDetail(ExecutableElement method) { - var args = new StringJoiner(", "); - var missingParamNames = - method.getParameters().stream().allMatch(p -> p.getSimpleName().toString().matches("arg\\d+")); - for (var p : method.getParameters()) { - if (missingParamNames) args.add(ShortTypePrinter.print(p.asType())); - else args.add(p.getSimpleName().toString()); - } - return String.format("%s %s(%s)", ShortTypePrinter.print(method.getReturnType()), method.getSimpleName(), args); - } - - private String asMarkdown(List<? extends DocTree> lines) { - var join = new StringJoiner("\n"); - for (var l : lines) join.add(l.toString()); - var html = join.toString(); - return Docs.htmlToMarkdown(html); - } - - private String asMarkdown(DocCommentTree comment) { - var lines = comment.getFirstSentence(); - return asMarkdown(lines); - } - - private MarkupContent asMarkupContent(DocCommentTree comment) { - var markdown = asMarkdown(comment); - var content = new MarkupContent(); - content.setKind(MarkupKind.MARKDOWN); - content.setValue(markdown); - return content; - } - - @Override - public CompletableFuture<CompletionItem> resolveCompletionItem(CompletionItem unresolved) { - var idJson = (JsonPrimitive) unresolved.getData(); - var id = idJson.getAsString(); - var cached = lastCompletions.get(id); - if (cached == null) { - LOG.warning("CompletionItem " + id + " was not in the cache"); - return CompletableFuture.completedFuture(unresolved); - } - if (cached.element != null) { - if (cached.element instanceof ExecutableElement) { - var method = (ExecutableElement) cached.element; - var tree = server.compiler.docs().methodTree(method); - var detail = tree.map(this::resolveDocDetail).orElse(resolveDefaultDetail(method)); - unresolved.setDetail(detail); - - var doc = server.compiler.docs().methodDoc(method); - var markdown = doc.map(this::asMarkupContent); - markdown.ifPresent(unresolved::setDocumentation); - } else if (cached.element instanceof TypeElement) { - var type = (TypeElement) cached.element; - var doc = server.compiler.docs().classDoc(type); - var markdown = doc.map(this::asMarkupContent); - markdown.ifPresent(unresolved::setDocumentation); - } else { - LOG.info("Don't know how to look up docs for element " + cached.element); - } - // TODO constructors, fields - } else if (cached.className != null) { - var doc = server.compiler.docs().classDoc(cached.className.name); - var markdown = doc.map(this::asMarkupContent); - markdown.ifPresent(unresolved::setDocumentation); - } - return CompletableFuture.completedFuture(unresolved); - } - - private String hoverTypeDeclaration(TypeElement t) { - var result = new StringBuilder(); - switch (t.getKind()) { - case ANNOTATION_TYPE: - result.append("@interface"); - break; - case INTERFACE: - result.append("interface"); - break; - case CLASS: - result.append("class"); - break; - case ENUM: - result.append("enum"); - break; - default: - LOG.warning("Don't know what to call type element " + t); - result.append("???"); - } - result.append(" ").append(ShortTypePrinter.print(t.asType())); - var superType = ShortTypePrinter.print(t.getSuperclass()); - switch (superType) { - case "Object": - case "none": - break; - default: - result.append(" extends ").append(superType); - } - return result.toString(); - } - - private String hoverCode(Element e) { - if (e instanceof ExecutableElement) { - var m = (ExecutableElement) e; - return ShortTypePrinter.printMethod(m); - } else if (e instanceof VariableElement) { - var v = (VariableElement) e; - return ShortTypePrinter.print(v.asType()) + " " + v; - } else if (e instanceof TypeElement) { - var t = (TypeElement) e; - var lines = new StringJoiner("\n"); - lines.add(hoverTypeDeclaration(t) + " {"); - for (var member : t.getEnclosedElements()) { - // TODO check accessibility - if (member instanceof ExecutableElement || member instanceof VariableElement) { - lines.add(" " + hoverCode(member) + ";"); - } else if (member instanceof TypeElement) { - lines.add(" " + hoverTypeDeclaration((TypeElement) member) + " { /* removed */ }"); - } - } - lines.add("}"); - return lines.toString(); - } else return e.toString(); - } - - private Optional<String> hoverDocs(Element e) { - if (e instanceof ExecutableElement) { - var m = (ExecutableElement) e; - return server.compiler.docs().methodDoc(m).map(this::asMarkdown); - } else if (e instanceof TypeElement) { - var t = (TypeElement) e; - return server.compiler.docs().classDoc(t).map(this::asMarkdown); - } else return Optional.empty(); - } - - private CompileFile hoverCache; - - private void updateHoverCache(URI uri, String contents) { - if (hoverCache == null || !hoverCache.file.equals(uri) || !hoverCache.contents.equals(contents)) { - LOG.info("File has changed since last hover, recompiling"); - hoverCache = server.compiler.compileFile(uri, contents); - } - } - - @Override - public CompletableFuture<Hover> hover(TextDocumentPositionParams position) { - // Compile entire file if it's changed since last hover - var uri = URI.create(position.getTextDocument().getUri()); - var content = contents(uri).content; - updateHoverCache(uri, content); - - // Find element undeer cursor - var line = position.getPosition().getLine() + 1; - var column = position.getPosition().getCharacter() + 1; - var el = hoverCache.element(line, column); - if (!el.isPresent()) return CompletableFuture.completedFuture(new Hover(Collections.emptyList())); - - // Add code hover message - var result = new ArrayList<Either<String, MarkedString>>(); - var code = hoverCode(el.get()); - result.add(Either.forRight(new MarkedString("java.hover", code))); - // Add docs hover message - var docs = hoverDocs(el.get()); - if (docs.isPresent()) { - result.add(Either.forLeft(docs.get())); - } - - return CompletableFuture.completedFuture(new Hover(result)); - } - - private List<ParameterInformation> signatureParamsFromDocs(MethodTree method, DocCommentTree doc) { - var ps = new ArrayList<ParameterInformation>(); - var paramComments = new HashMap<String, String>(); - for (var tag : doc.getBlockTags()) { - if (tag.getKind() == DocTree.Kind.PARAM) { - var param = (ParamTree) tag; - paramComments.put(param.getName().toString(), asMarkdown(param.getDescription())); - } - } - for (var param : method.getParameters()) { - var info = new ParameterInformation(); - var name = param.getName().toString(); - info.setLabel(name); - if (paramComments.containsKey(name)) info.setDocumentation(paramComments.get(name)); - else info.setDocumentation(Objects.toString(param.getType(), "")); - ps.add(info); - } - return ps; - } - - private List<ParameterInformation> signatureParamsFromMethod(ExecutableElement e) { - var missingParamNames = ShortTypePrinter.missingParamNames(e); - var ps = new ArrayList<ParameterInformation>(); - for (var v : e.getParameters()) { - var p = new ParameterInformation(); - if (missingParamNames) p.setLabel(ShortTypePrinter.print(v.asType())); - else p.setLabel(v.getSimpleName().toString()); - ps.add(p); - } - return ps; - } - - private SignatureInformation asSignatureInformation(ExecutableElement e) { - var i = new SignatureInformation(); - var ps = signatureParamsFromMethod(e); - var doc = server.compiler.docs().methodDoc(e); - var tree = server.compiler.docs().methodTree(e); - if (doc.isPresent() && tree.isPresent()) ps = signatureParamsFromDocs(tree.get(), doc.get()); - var args = ps.stream().map(p -> p.getLabel()).collect(Collectors.joining(", ")); - var name = e.getSimpleName().toString(); - if (name.equals("<init>")) name = e.getEnclosingElement().getSimpleName().toString(); - i.setLabel(name + "(" + args + ")"); - i.setParameters(ps); - return i; - } - - private SignatureHelp asSignatureHelp(MethodInvocation invoke) { - // TODO use docs to get parameter names - var sigs = new ArrayList<SignatureInformation>(); - for (var e : invoke.overloads) { - sigs.add(asSignatureInformation(e)); - } - var activeSig = invoke.activeMethod.map(invoke.overloads::indexOf).orElse(0); - return new SignatureHelp(sigs, activeSig, invoke.activeParameter); - } - - @Override - public CompletableFuture<SignatureHelp> signatureHelp(TextDocumentPositionParams position) { - var uri = URI.create(position.getTextDocument().getUri()); - var content = contents(uri).content; - var line = position.getPosition().getLine() + 1; - var column = position.getPosition().getCharacter() + 1; - var help = - server.compiler - .compileFocus(uri, content, line, column) - .methodInvocation() - .map(this::asSignatureHelp) - .orElse(new SignatureHelp()); - return CompletableFuture.completedFuture(help); - } - - @Override - public CompletableFuture<List<? extends Location>> definition(TextDocumentPositionParams position) { - var fromUri = URI.create(position.getTextDocument().getUri()); - var fromLine = position.getPosition().getLine() + 1; - var fromColumn = position.getPosition().getCharacter() + 1; - var fromContent = contents(fromUri).content; - var fromFocus = server.compiler.compileFocus(fromUri, fromContent, fromLine, fromColumn); - var toEl = fromFocus.element(); - var toUri = fromFocus.declaringFile(toEl); - if (!toUri.isPresent()) return CompletableFuture.completedFuture(List.of()); - var toContent = contents(toUri.get()).content; - var toFile = server.compiler.compileFile(toUri.get(), toContent); - var toPath = toFile.find(new Ptr(toEl)); - if (!toPath.isPresent()) return CompletableFuture.completedFuture(List.of()); - // Figure out where in the file the definition is - var toRange = toFile.range(toPath.get()); - if (!toRange.isPresent()) return CompletableFuture.completedFuture(List.of()); - var to = new Location(toUri.get().toString(), toRange.get()); - return CompletableFuture.completedFuture(List.of(to)); - } - - class ReportProgress implements ReportReferencesProgress, AutoCloseable { - private final Function<Integer, String> scanMessage, checkMessage; - - ReportProgress( - String startMessage, Function<Integer, String> scanMessage, Function<Integer, String> checkMessage) { - this.scanMessage = scanMessage; - this.checkMessage = checkMessage; - server.client().javaStartProgress(new JavaStartProgressParams(startMessage)); - } - - private int percent(int n, int d) { - double nD = n, dD = d; - double ratio = nD / dD; - return (int) (ratio * 100); - } - - public void scanForPotentialReferences(int nScanned, int nFiles) { - var message = scanMessage.apply(nFiles); - if (nScanned == 0) { - server.client().javaReportProgress(new JavaReportProgressParams(message)); - } else { - var increment = percent(nScanned, nFiles) > percent(nScanned - 1, nFiles) ? 1 : 0; - server.client().javaReportProgress(new JavaReportProgressParams(message, increment)); - } - } - - public void checkPotentialReferences(int nCompiled, int nPotential) { - var message = checkMessage.apply(nCompiled); - if (nCompiled == 0) { - server.client().javaReportProgress(new JavaReportProgressParams(message)); - } else { - var increment = percent(nCompiled, nPotential) > percent(nCompiled - 1, nPotential) ? 1 : 0; - server.client().javaReportProgress(new JavaReportProgressParams(message, increment)); - } - } - - @Override - public void close() { - server.client().javaEndProgress(); - } - } - - @Override - public CompletableFuture<List<? extends Location>> references(ReferenceParams position) { - var toUri = URI.create(position.getTextDocument().getUri()); - var toContent = contents(toUri).content; - var toLine = position.getPosition().getLine() + 1; - var toColumn = position.getPosition().getCharacter() + 1; - var toEl = server.compiler.compileFocus(toUri, toContent, toLine, toColumn).element(); - var fromFiles = server.compiler.potentialReferences(toEl); - if (fromFiles.isEmpty()) return CompletableFuture.completedFuture(List.of()); - var batch = server.compiler.compileBatch(fromFiles); - var fromTreePaths = batch.references(toEl); - var result = new ArrayList<Location>(); - for (var path : fromTreePaths) { - var fromUri = path.getCompilationUnit().getSourceFile().toUri(); - var fromRange = batch.range(path); - if (!fromRange.isPresent()) { - LOG.warning(String.format("Couldn't locate `%s`", path.getLeaf())); - continue; - } - var from = new Location(fromUri.toString(), fromRange.get()); - result.add(from); - } - return CompletableFuture.completedFuture(result); - } - - @Override - public CompletableFuture<List<? extends DocumentHighlight>> documentHighlight(TextDocumentPositionParams position) { - return null; - } - - @Override - public CompletableFuture<List<Either<SymbolInformation, DocumentSymbol>>> documentSymbol( - DocumentSymbolParams params) { - var uri = URI.create(params.getTextDocument().getUri()); - var content = contents(uri).content; - var result = - Parser.documentSymbols(Paths.get(uri), content) - .stream() - .map(Parser::asSymbolInformation) - .map(Either::<SymbolInformation, DocumentSymbol>forLeft) - .collect(Collectors.toList()); - return CompletableFuture.completedFuture(result); - } - - @Override - public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) { - return null; - } - - @Override - public CompletableFuture<List<? extends CodeLens>> codeLens(CodeLensParams params) { - // TODO just create a blank code lens on every method, then resolve it async - var uri = URI.create(params.getTextDocument().getUri()); - var content = contents(uri).content; - var parse = server.compiler.parseFile(uri, content); - var declarations = parse.declarations(); - var result = new ArrayList<CodeLens>(); - for (var d : declarations) { - var range = parse.range(d); - if (!range.isPresent()) continue; - var className = JavaCompilerService.className(d); - var memberName = JavaCompilerService.memberName(d); - // If test class or method, add "Run Test" code lens - if (parse.isTestClass(d)) { - var command = - new Command("Run All Tests", "java.command.test.run", Arrays.asList(uri, className, null)); - var lens = new CodeLens(range.get(), command, null); - result.add(lens); - // TODO run all tests in file - // TODO run all tests in package - } else if (parse.isTestMethod(d)) { - var command = - new Command( - "Run Test", - "java.command.test.run", - Arrays.asList(uri, className, memberName.orElse(null))); - var lens = new CodeLens(range.get(), command, null); - result.add(lens); - } - // If method or field, add an unresolved "_ references" code lens - if (memberName.isPresent()) { - var start = range.get().getStart(); - var line = start.getLine(); - var character = start.getCharacter(); - List<Object> data = List.of("java.command.findReferences", uri, line, character, new Ptr(d).toString()); - var lens = new CodeLens(range.get(), null, data); - result.add(lens); - } - } - return CompletableFuture.completedFuture(result); - } - - private Map<Ptr, Integer> cacheCountReferences = Collections.emptyMap(); - private URI cacheCountReferencesFile = URI.create("file:///NONE"); - - private void updateCacheCountReferences(URI current) { - if (cacheCountReferencesFile.equals(current)) return; - LOG.info(String.format("Update cached reference count to %s...", current)); - var content = contents(current).content; - cacheCountReferences = server.compiler.countReferences(current, content); - cacheCountReferencesFile = current; - } - - @Override - public CompletableFuture<CodeLens> resolveCodeLens(CodeLens unresolved) { - // Unpack data - var data = (JsonArray) unresolved.getData(); - var command = data.get(0).getAsString(); - assert command.equals("java.command.findReferences"); - var uriString = data.get(1).getAsString(); - var line = data.get(2).getAsInt(); - var character = data.get(3).getAsInt(); - var ptrString = data.get(4).getAsString(); - // Parse data - var uri = URI.create(uriString); - var ptr = new Ptr(ptrString); - // Update cache if necessary - updateCacheCountReferences(uri); - // Read reference count from cache - var count = cacheCountReferences.getOrDefault(ptr, 0); - // Update command - String title; - if (count == 1) title = "1 reference"; - else title = String.format("%d references", count); - unresolved.setCommand(new Command(title, command, List.of(uri, line, character))); - - return CompletableFuture.completedFuture(unresolved); - } - - private List<TextEdit> fixImports(URI java) { - var contents = server.textDocuments.contents(java).content; - var fix = server.compiler.compileFile(java, contents).fixImports(); - // TODO if imports already match fixed-imports, return empty list - // TODO preserve comments and other details of existing imports - var edits = new ArrayList<TextEdit>(); - // Delete all existing imports - for (var i : fix.parsed.getImports()) { - if (!i.isStatic()) { - var offset = fix.sourcePositions.getStartPosition(fix.parsed, i); - var line = (int) fix.parsed.getLineMap().getLineNumber(offset) - 1; - var delete = new TextEdit(new Range(new Position(line, 0), new Position(line + 1, 0)), ""); - edits.add(delete); - } - } - if (fix.fixedImports.isEmpty()) return edits; - // Find a place to insert the new imports - long insertLine = -1; - var insertText = new StringBuilder(); - // If there are imports, use the start of the first import as the insert position - for (var i : fix.parsed.getImports()) { - if (!i.isStatic() && insertLine == -1) { - long offset = fix.sourcePositions.getStartPosition(fix.parsed, i); - insertLine = fix.parsed.getLineMap().getLineNumber(offset) - 1; - } - } - // If there are no imports, insert after the package declaration - if (insertLine == -1 && fix.parsed.getPackageName() != null) { - long offset = fix.sourcePositions.getEndPosition(fix.parsed, fix.parsed.getPackageName()); - insertLine = fix.parsed.getLineMap().getLineNumber(offset); - insertText.append("\n"); - } - // If there are no imports and no package, insert at the top of the file - if (insertLine == -1) { - insertLine = 0; - } - // Insert each import - fix.fixedImports - .stream() - .sorted() - .forEach( - i -> { - insertText.append("import ").append(i).append(";\n"); - }); - var insertPosition = new Position((int) insertLine, 0); - var insert = new TextEdit(new Range(insertPosition, insertPosition), insertText.toString()); - edits.add(insert); - return edits; - } - - @Override - public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params) { - var uri = URI.create(params.getTextDocument().getUri()); - return CompletableFuture.completedFuture(fixImports(uri)); - } - - @Override - public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params) { - return null; - } - - @Override - public CompletableFuture<List<? extends TextEdit>> onTypeFormatting(DocumentOnTypeFormattingParams params) { - return null; - } - - @Override - public CompletableFuture<WorkspaceEdit> rename(RenameParams params) { - return null; - } - - private boolean isJava(URI uri) { - return uri.getPath().endsWith(".java"); - } - - @Override - public void didOpen(DidOpenTextDocumentParams params) { - var document = params.getTextDocument(); - var uri = URI.create(document.getUri()); - if (isJava(uri)) { - activeDocuments.put(uri, new VersionedContent(document.getText(), document.getVersion())); - server.lint(Collections.singleton(uri)); - } - } - - @Override - public void didChange(DidChangeTextDocumentParams params) { - var document = params.getTextDocument(); - var uri = URI.create(document.getUri()); - if (isJava(uri)) { - var existing = activeDocuments.get(uri); - var newText = existing.content; - - if (document.getVersion() > existing.version) { - for (var change : params.getContentChanges()) { - if (change.getRange() == null) newText = change.getText(); - else newText = patch(newText, change); - } - - activeDocuments.put(uri, new VersionedContent(newText, document.getVersion())); - } else LOG.warning("Ignored change with version " + document.getVersion() + " <= " + existing.version); - } - } - - private String patch(String sourceText, TextDocumentContentChangeEvent change) { - try { - var range = change.getRange(); - var reader = new BufferedReader(new StringReader(sourceText)); - var writer = new StringWriter(); - - // Skip unchanged lines - int line = 0; - - while (line < range.getStart().getLine()) { - writer.write(reader.readLine() + '\n'); - line++; - } - - // Skip unchanged chars - for (int character = 0; character < range.getStart().getCharacter(); character++) - writer.write(reader.read()); - - // Write replacement text - writer.write(change.getText()); - - // Skip replaced text - reader.skip(change.getRangeLength()); - - // Write remaining text - while (true) { - int next = reader.read(); - - if (next == -1) return writer.toString(); - else writer.write(next); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public void didClose(DidCloseTextDocumentParams params) { - var document = params.getTextDocument(); - var uri = URI.create(document.getUri()); - if (isJava(uri)) { - // Remove from source cache - activeDocuments.remove(uri); - - // Clear diagnostics - server.publishDiagnostics(Collections.singletonList(uri), Collections.emptyList()); - } - } - - @Override - public void didSave(DidSaveTextDocumentParams params) { - var uri = URI.create(params.getTextDocument().getUri()); - if (isJava(uri)) { - // Re-lint all active documents - server.lint(activeDocuments.keySet()); - // TODO update config when java file implies a new source root - } - // TODO update config when pom.xml changes - } - - Set<URI> activeDocuments() { - return activeDocuments.keySet(); - } - - VersionedContent contents(URI openFile) { - if (activeDocuments.containsKey(openFile)) { - return activeDocuments.get(openFile); - } else { - try { - var content = Files.readAllLines(Paths.get(openFile)).stream().collect(Collectors.joining("\n")); - return new VersionedContent(content, -1); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private static final Logger LOG = Logger.getLogger("main"); -} diff --git a/src/main/java/org/javacs/JavaWorkspaceService.java b/src/main/java/org/javacs/JavaWorkspaceService.java deleted file mode 100644 index f605396..0000000 --- a/src/main/java/org/javacs/JavaWorkspaceService.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.javacs; - -import com.google.gson.JsonObject; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.HashSet; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import org.javacs.lsp.*; - -class JavaWorkspaceService implements WorkspaceService { - private static final Logger LOG = Logger.getLogger("main"); - - private final JavaLanguageServer server; - - JavaWorkspaceService(JavaLanguageServer server) { - this.server = server; - } - - @Override - public CompletableFuture<Object> executeCommand(ExecuteCommandParams params) { - return null; - } - - @Override - public CompletableFuture<List<? extends SymbolInformation>> symbol(WorkspaceSymbolParams params) { - List<SymbolInformation> list = - server.compiler - .findSymbols(params.getQuery()) - .map(Parser::asSymbolInformation) - .limit(50) - .collect(Collectors.toList()); - return CompletableFuture.completedFuture(list); - } - - @Override - public void didChangeConfiguration(DidChangeConfigurationParams change) { - var settings = (JsonObject) change.getSettings(); - var java = settings.getAsJsonObject("java"); - - var externalDependencies = java.getAsJsonArray("externalDependencies"); - var strings = new HashSet<String>(); - for (var each : externalDependencies) strings.add(each.getAsString()); - server.setExternalDependencies(strings); - - var classPath = java.getAsJsonArray("classPath"); - var paths = new HashSet<Path>(); - for (var each : classPath) paths.add(Paths.get(each.getAsString()).toAbsolutePath()); - server.setClassPath(paths); - } - - @Override - public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) {} -} diff --git a/src/main/java/org/javacs/Main.java b/src/main/java/org/javacs/Main.java index 985c17b..f8dd455 100644 --- a/src/main/java/org/javacs/Main.java +++ b/src/main/java/org/javacs/Main.java @@ -1,6 +1,5 @@ package org.javacs; -import java.util.concurrent.Executors; import java.util.logging.Level; import java.util.logging.Logger; import org.javacs.lsp.*; @@ -14,25 +13,16 @@ public class Main { for (var h : root.getHandlers()) h.setFormatter(new LogFormat()); } + private JavaLanguageServer createServer(LanguageClient client) { + return new JavaLanguageServer(client); + } + public static void main(String[] args) { try { // Logger.getLogger("").addHandler(new FileHandler("javacs.%u.log", false)); setRootFormat(); - var server = new JavaLanguageServer(); - var threads = Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "client")); - var launcher = - new Launcher.Builder<CustomLanguageClient>() - .setLocalService(server) - .setRemoteInterface(CustomLanguageClient.class) - .setInput(System.in) - .setOutput(System.out) - .setExecutorService(threads) - .create(); - - server.installClient(launcher.getRemoteProxy()); - launcher.startListening(); - LOG.info(String.format("java.version is %s", System.getProperty("java.version"))); + LSP.connect(JavaLanguageServer::new, System.in, System.out); } catch (Throwable t) { LOG.log(Level.SEVERE, t.getMessage(), t); diff --git a/src/main/java/org/javacs/Parser.java b/src/main/java/org/javacs/Parser.java index 13cf305..f67b0af 100644 --- a/src/main/java/org/javacs/Parser.java +++ b/src/main/java/org/javacs/Parser.java @@ -91,7 +91,7 @@ class Parser { return true; } - private static void onError(Diagnostic<? extends JavaFileObject> err) { + private static void onError(javax.tools.Diagnostic<? extends JavaFileObject> err) { // Too noisy, this only comes up in parse tasks which tend to be less important // LOG.warning(err.getMessage(Locale.getDefault())); } @@ -185,11 +185,10 @@ class Parser { int startLine = (int) lines.getLineNumber(start) - 1, startCol = (int) lines.getColumnNumber(start) - 1; int endLine = (int) lines.getLineNumber(end) - 1, endCol = (int) lines.getColumnNumber(end) - 1; var dUri = cu.getSourceFile().toUri(); - return new Location( - dUri.toString(), new Range(new Position(startLine, startCol), new Position(endLine, endCol))); + return new Location(dUri, new Range(new Position(startLine, startCol), new Position(endLine, endCol))); } - private static SymbolKind asSymbolKind(Tree.Kind k) { + private static Integer asSymbolKind(Tree.Kind k) { switch (k) { case ANNOTATION_TYPE: case CLASS: @@ -247,10 +246,10 @@ class Parser { static SymbolInformation asSymbolInformation(TreePath path) { var i = new SymbolInformation(); var t = path.getLeaf(); - i.setKind(asSymbolKind(t.getKind())); - i.setName(symbolName(t)); - i.setContainerName(containerName(path)); - i.setLocation(Parser.location(path)); + i.kind = asSymbolKind(t.getKind()); + i.name = symbolName(t); + i.containerName = containerName(path); + i.location = Parser.location(path); return i; } diff --git a/src/main/java/org/javacs/lsp/CancelParams.java b/src/main/java/org/javacs/lsp/CancelParams.java index cf7acce..cb75622 100644 --- a/src/main/java/org/javacs/lsp/CancelParams.java +++ b/src/main/java/org/javacs/lsp/CancelParams.java @@ -1,5 +1,5 @@ package org.javacs.lsp; public class CancelParams { - public String id; + public int id; } diff --git a/src/main/java/org/javacs/lsp/CodeLens.java b/src/main/java/org/javacs/lsp/CodeLens.java index 79158a7..31da084 100644 --- a/src/main/java/org/javacs/lsp/CodeLens.java +++ b/src/main/java/org/javacs/lsp/CodeLens.java @@ -6,4 +6,12 @@ public class CodeLens { public Range range; public Command command; public JsonArray data; + + public CodeLens() {} + + public CodeLens(Range range, Command command, JsonArray data) { + this.range = range; + this.command = command; + this.data = data; + } } diff --git a/src/main/java/org/javacs/lsp/CodeLensParams.java b/src/main/java/org/javacs/lsp/CodeLensParams.java index 7958ac7..65b4afd 100644 --- a/src/main/java/org/javacs/lsp/CodeLensParams.java +++ b/src/main/java/org/javacs/lsp/CodeLensParams.java @@ -2,4 +2,10 @@ package org.javacs.lsp; public class CodeLensParams { public TextDocumentIdentifier textDocument; + + public CodeLensParams() {} + + public CodeLensParams(TextDocumentIdentifier textDocument) { + this.textDocument = textDocument; + } } diff --git a/src/main/java/org/javacs/lsp/Command.java b/src/main/java/org/javacs/lsp/Command.java index a212c92..979d6cc 100644 --- a/src/main/java/org/javacs/lsp/Command.java +++ b/src/main/java/org/javacs/lsp/Command.java @@ -5,4 +5,12 @@ import com.google.gson.JsonArray; public class Command { public String title, command; public JsonArray arguments; + + public Command() {} + + public Command(String title, String command, JsonArray arguments) { + this.title = title; + this.command = command; + this.arguments = arguments; + } } diff --git a/src/main/java/org/javacs/lsp/CompletionContext.java b/src/main/java/org/javacs/lsp/CompletionContext.java deleted file mode 100644 index f10547f..0000000 --- a/src/main/java/org/javacs/lsp/CompletionContext.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.javacs.lsp; - -public class CompletionContext { - public int triggerKind; - public String triggerCharacter; -} diff --git a/src/main/java/org/javacs/lsp/CompletionItem.java b/src/main/java/org/javacs/lsp/CompletionItem.java index 1a46753..678a535 100644 --- a/src/main/java/org/javacs/lsp/CompletionItem.java +++ b/src/main/java/org/javacs/lsp/CompletionItem.java @@ -1,6 +1,6 @@ package org.javacs.lsp; -import com.google.gson.JsonArray; +import com.google.gson.JsonElement; import java.util.List; public class CompletionItem { @@ -9,10 +9,11 @@ public class CompletionItem { public String detail; public MarkupContent documentation; public boolean deprecated, preselect; - public String sortText, filterText, insertText, insertTextFormat; + public String sortText, filterText, insertText; + public int insertTextFormat; public TextEdit textEdit; public List<TextEdit> additionalTextEdits; public List<Character> commitCharacters; public Command command; - public JsonArray data; + public JsonElement data; } diff --git a/src/main/java/org/javacs/lsp/CompletionList.java b/src/main/java/org/javacs/lsp/CompletionList.java index 48d3155..777a80f 100644 --- a/src/main/java/org/javacs/lsp/CompletionList.java +++ b/src/main/java/org/javacs/lsp/CompletionList.java @@ -5,4 +5,11 @@ import java.util.List; public class CompletionList { public boolean isIncomplete; public List<CompletionItem> items; + + public CompletionList() {} + + public CompletionList(boolean isIncomplete, List<CompletionItem> items) { + this.isIncomplete = isIncomplete; + this.items = items; + } } diff --git a/src/main/java/org/javacs/lsp/CompletionParams.java b/src/main/java/org/javacs/lsp/CompletionParams.java deleted file mode 100644 index c3dd2ef..0000000 --- a/src/main/java/org/javacs/lsp/CompletionParams.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.javacs.lsp; - -public class CompletionParams extends TextDocumentPositionParams { - public CompletionContext context; -} diff --git a/src/main/java/org/javacs/lsp/Diagnostic.java b/src/main/java/org/javacs/lsp/Diagnostic.java index 75986f7..404957e 100644 --- a/src/main/java/org/javacs/lsp/Diagnostic.java +++ b/src/main/java/org/javacs/lsp/Diagnostic.java @@ -2,6 +2,6 @@ package org.javacs.lsp; public class Diagnostic { public Range range; - public int severity; + public Integer severity; public String code, source, message; } diff --git a/src/main/java/org/javacs/lsp/DidOpenTextDocumentParams.java b/src/main/java/org/javacs/lsp/DidOpenTextDocumentParams.java index 1083a95..1c12d1e 100644 --- a/src/main/java/org/javacs/lsp/DidOpenTextDocumentParams.java +++ b/src/main/java/org/javacs/lsp/DidOpenTextDocumentParams.java @@ -2,4 +2,10 @@ package org.javacs.lsp; public class DidOpenTextDocumentParams { public TextDocumentItem textDocument; + + public DidOpenTextDocumentParams() {} + + public DidOpenTextDocumentParams(TextDocumentItem textDocument) { + this.textDocument = textDocument; + } } diff --git a/src/main/java/org/javacs/lsp/DocumentFormattingParams.java b/src/main/java/org/javacs/lsp/DocumentFormattingParams.java new file mode 100644 index 0000000..4ccee64 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DocumentFormattingParams.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class DocumentFormattingParams { + public TextDocumentIdentifier textDocument; + public FormattingOptions options; +} diff --git a/src/main/java/org/javacs/lsp/DocumentSymbolParams.java b/src/main/java/org/javacs/lsp/DocumentSymbolParams.java index ff8e740..9670d50 100644 --- a/src/main/java/org/javacs/lsp/DocumentSymbolParams.java +++ b/src/main/java/org/javacs/lsp/DocumentSymbolParams.java @@ -2,4 +2,10 @@ package org.javacs.lsp; public class DocumentSymbolParams { public TextDocumentIdentifier textDocument; + + public DocumentSymbolParams() {} + + public DocumentSymbolParams(TextDocumentIdentifier textDocument) { + this.textDocument = textDocument; + } } diff --git a/src/main/java/org/javacs/lsp/FormattingOptions.java b/src/main/java/org/javacs/lsp/FormattingOptions.java new file mode 100644 index 0000000..28f0da3 --- /dev/null +++ b/src/main/java/org/javacs/lsp/FormattingOptions.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +public class FormattingOptions { + public int tabSize; + public boolean insertSpaces; + // TODO other properties embedded at same level +} diff --git a/src/main/java/org/javacs/lsp/Hover.java b/src/main/java/org/javacs/lsp/Hover.java index 25dadd0..972c408 100644 --- a/src/main/java/org/javacs/lsp/Hover.java +++ b/src/main/java/org/javacs/lsp/Hover.java @@ -5,4 +5,15 @@ import java.util.List; public class Hover { public List<MarkedString> contents; public Range range; + + public Hover() {} + + public Hover(List<MarkedString> contents) { + this.contents = contents; + } + + public Hover(List<MarkedString> contents, Range range) { + this.contents = contents; + this.range = range; + } } diff --git a/src/main/java/org/javacs/lsp/InitializeResult.java b/src/main/java/org/javacs/lsp/InitializeResult.java index a8c8cee..8a56e2f 100644 --- a/src/main/java/org/javacs/lsp/InitializeResult.java +++ b/src/main/java/org/javacs/lsp/InitializeResult.java @@ -4,4 +4,10 @@ import com.google.gson.JsonObject; public class InitializeResult { public JsonObject capabilities; + + public InitializeResult() {} + + public InitializeResult(JsonObject capabilities) { + this.capabilities = capabilities; + } } diff --git a/src/main/java/org/javacs/lsp/LSP.java b/src/main/java/org/javacs/lsp/LSP.java index 06da2d7..f9b4572 100644 --- a/src/main/java/org/javacs/lsp/LSP.java +++ b/src/main/java/org/javacs/lsp/LSP.java @@ -4,8 +4,10 @@ import com.google.common.base.Charsets; import com.google.gson.Gson; import com.google.gson.JsonElement; import java.io.*; +import java.util.Optional; import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; @@ -15,13 +17,13 @@ public class LSP { private static String readHeader(InputStream client) { var line = new StringBuilder(); - for (var next = read(client); next != -1; next = read(client)) { + for (var next = read(client); true; next = read(client)) { if (next == '\r') { var last = read(client); assert last == '\n'; break; } - line.append((char) next); + line.append(next); } return line.toString(); } @@ -36,9 +38,16 @@ public class LSP { return -1; } - private static int read(InputStream client) { + static class EndOfStream extends RuntimeException {} + + private static char read(InputStream client) { try { - return client.read(); + var c = client.read(); + if (c == -1) { + LOG.warning("Stream from client has been closed, throwing kill exception..."); + throw new EndOfStream(); + } + return (char) c; } catch (IOException e) { throw new RuntimeException(e); } @@ -48,15 +57,14 @@ public class LSP { // Eat whitespace // Have observed problems with extra \r\n sequences from VSCode var next = read(client); - while (next != -1 && Character.isWhitespace(next)) { + while (Character.isWhitespace(next)) { next = read(client); } // Append next var result = new StringBuilder(); var i = 0; while (true) { - if (next == -1) break; - result.append((char) next); + result.append(next); i++; if (i == byteLength) break; next = read(client); @@ -93,12 +101,20 @@ public class LSP { } static void respond(OutputStream client, int requestId, Object params) { + if (params instanceof Optional) { + var option = (Optional) params; + params = option.orElse(null); + } var jsonText = gson.toJson(params); var messageText = String.format("{\"jsonrpc\":\"2.0\",\"id\":%d,\"result\":%s}", requestId, jsonText); writeClient(client, messageText); } private static void notifyClient(OutputStream client, String method, Object params) { + if (params instanceof Optional) { + var option = (Optional) params; + params = option.orElse(null); + } var jsonText = gson.toJson(params); var messageText = String.format("{\"jsonrpc\":\"2.0\",\"method\":\"%s\",\"params\":%s}", method, jsonText); writeClient(client, messageText); @@ -113,49 +129,58 @@ public class LSP { @Override public void publishDiagnostics(PublishDiagnosticsParams params) { - var json = gson.toJson(params); - notifyClient(send, "textDocument/publishDiagnostics", json); + notifyClient(send, "textDocument/publishDiagnostics", params); } @Override public void showMessage(ShowMessageParams params) { - var json = gson.toJson(params); - notifyClient(send, "window/showMessage", json); + notifyClient(send, "window/showMessage", params); } @Override public void registerCapability(String id, JsonElement options) { - var p = new RegistrationParams(); - p.id = UUID.randomUUID().toString(); - p.id = id; - p.registerOptions = options; - var json = gson.toJson(p); - notifyClient(send, "client/registerCapability", json); + var params = new RegistrationParams(); + params.id = UUID.randomUUID().toString(); + params.id = id; + params.registerOptions = options; + + notifyClient(send, "client/registerCapability", params); } @Override public void customNotification(String method, JsonElement params) { - var json = gson.toJson(params); - notifyClient(send, method, json); + notifyClient(send, method, params); } } public static void connect( Function<LanguageClient, LanguageServer> serverFactory, InputStream receive, OutputStream send) { var server = serverFactory.apply(new RealClient(send)); - var pendingRequests = new ArrayBlockingQueue<Message>(10); + var pending = new ArrayBlockingQueue<Message>(10); + var endOfStream = new Message(); // Read messages and process cancellations on a separate thread class MessageReader implements Runnable { void peek(Message message) { if (message.method.equals("$/cancelRequest")) { var params = gson.fromJson(message.params, CancelParams.class); - var removed = pendingRequests.removeIf(r -> r.id.equals(params.id)); + var removed = pending.removeIf(r -> r.id != null && r.id.equals(params.id)); if (removed) LOG.info(String.format("Cancelled request %d, which had not yet started", params.id)); else LOG.info(String.format("Cannot cancel request %d because it has already started", params.id)); } } + private boolean kill() { + LOG.info("Read stream has been closed, putting kill message onto queue..."); + try { + pending.put(endOfStream); + return true; + } catch (Exception e) { + LOG.log(Level.SEVERE, "Failed to put kill message onto queue, will try again...", e); + return false; + } + } + @Override public void run() { LOG.info("Placing incoming messages on queue..."); @@ -165,10 +190,9 @@ public class LSP { var token = nextToken(receive); var message = parseMessage(token); peek(message); - pendingRequests.put(message); - LOG.info( - String.format( - "Added message %d to queue, length is %d", message.id, pendingRequests.size())); + pending.put(message); + } catch (EndOfStream __) { + if (kill()) return; } catch (Exception e) { LOG.log(Level.SEVERE, e.getMessage(), e); } @@ -184,14 +208,22 @@ public class LSP { processMessages: while (true) { try { - var request = pendingRequests.take(); - LOG.info(String.format("Read message %d from queue, length is %d", request.id, pendingRequests.size())); - switch (request.method) { + // Take a break every 1s to check if receive has been closed + var r = pending.poll(1, TimeUnit.SECONDS); + // If receive has been closed, exit + if (r == endOfStream) { + LOG.warning("Stream from client has been closed, exiting..."); + break processMessages; + } + // If poll(_) failed, loop again + if (r == null) continue; + // Otherwise, process the new message + switch (r.method) { case "initialize": { - var params = gson.fromJson(request.params, InitializeParams.class); + var params = gson.fromJson(r.params, InitializeParams.class); var response = server.initialize(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "initialized": @@ -211,145 +243,152 @@ public class LSP { } case "workspace/didChangeWorkspaceFolders": { - var params = gson.fromJson(request.params, DidChangeWorkspaceFoldersParams.class); + var params = gson.fromJson(r.params, DidChangeWorkspaceFoldersParams.class); server.didChangeWorkspaceFolders(params); break; } case "workspace/didChangeConfiguration": { - var params = gson.fromJson(request.params, DidChangeConfigurationParams.class); + var params = gson.fromJson(r.params, DidChangeConfigurationParams.class); server.didChangeConfiguration(params); break; } case "workspace/didChangeWatchedFiles": { - var params = gson.fromJson(request.params, DidChangeWatchedFilesParams.class); + var params = gson.fromJson(r.params, DidChangeWatchedFilesParams.class); server.didChangeWatchedFiles(params); break; } case "workspace/symbols": { - var params = gson.fromJson(request.params, WorkspaceSymbolParams.class); + var params = gson.fromJson(r.params, WorkspaceSymbolParams.class); var response = server.workspaceSymbols(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/didOpen": { - var params = gson.fromJson(request.params, DidOpenTextDocumentParams.class); + var params = gson.fromJson(r.params, DidOpenTextDocumentParams.class); server.didOpenTextDocument(params); break; } case "textDocument/didChange": { - var params = gson.fromJson(request.params, DidChangeTextDocumentParams.class); + var params = gson.fromJson(r.params, DidChangeTextDocumentParams.class); server.didChangeTextDocument(params); break; } case "textDocument/willSave": { - var params = gson.fromJson(request.params, WillSaveTextDocumentParams.class); + var params = gson.fromJson(r.params, WillSaveTextDocumentParams.class); server.willSaveTextDocument(params); break; } case "textDocument/willSaveWaitUntil": { - var params = gson.fromJson(request.params, WillSaveTextDocumentParams.class); + var params = gson.fromJson(r.params, WillSaveTextDocumentParams.class); var response = server.willSaveWaitUntilTextDocument(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/didSave": { - var params = gson.fromJson(request.params, DidSaveTextDocumentParams.class); + var params = gson.fromJson(r.params, DidSaveTextDocumentParams.class); server.didSaveTextDocument(params); break; } case "textDocument/didClose": { - var params = gson.fromJson(request.params, DidCloseTextDocumentParams.class); + var params = gson.fromJson(r.params, DidCloseTextDocumentParams.class); server.didCloseTextDocument(params); break; } case "textDocument/completion": { - var params = gson.fromJson(request.params, CompletionParams.class); + var params = gson.fromJson(r.params, TextDocumentPositionParams.class); var response = server.completion(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "completionItem/resolve": { - var params = gson.fromJson(request.params, CompletionItem.class); + var params = gson.fromJson(r.params, CompletionItem.class); var response = server.resolveCompletionItem(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/hover": { - var params = gson.fromJson(request.params, TextDocumentPositionParams.class); + var params = gson.fromJson(r.params, TextDocumentPositionParams.class); var response = server.hover(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/signatureHelp": { - var params = gson.fromJson(request.params, TextDocumentPositionParams.class); + var params = gson.fromJson(r.params, TextDocumentPositionParams.class); var response = server.signatureHelp(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/definition": { - var params = gson.fromJson(request.params, TextDocumentPositionParams.class); + var params = gson.fromJson(r.params, TextDocumentPositionParams.class); var response = server.gotoDefinition(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/references": { - var params = gson.fromJson(request.params, ReferenceParams.class); + var params = gson.fromJson(r.params, ReferenceParams.class); var response = server.findReferences(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/documentSymbol": { - var params = gson.fromJson(request.params, DocumentSymbolParams.class); + var params = gson.fromJson(r.params, DocumentSymbolParams.class); var response = server.documentSymbol(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/codeAction": { - var params = gson.fromJson(request.params, CodeActionParams.class); + var params = gson.fromJson(r.params, CodeActionParams.class); var response = server.codeAction(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/codeLens": { - var params = gson.fromJson(request.params, CodeLensParams.class); + var params = gson.fromJson(r.params, CodeLensParams.class); var response = server.codeLens(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "codeLens/resolve": { - var params = gson.fromJson(request.params, CodeLens.class); + var params = gson.fromJson(r.params, CodeLens.class); var response = server.resolveCodeLens(params); - respond(send, request.id, response); + respond(send, r.id, response); break; } case "textDocument/rename": { - var params = gson.fromJson(request.params, RenameParams.class); + var params = gson.fromJson(r.params, RenameParams.class); var response = server.rename(params); - respond(send, request.id, response); + respond(send, r.id, response); + break; + } + case "textDocument/formatting": + { + var params = gson.fromJson(r.params, DocumentFormattingParams.class); + var response = server.formatting(params); + respond(send, r.id, response); break; } default: - LOG.warning(String.format("Don't know what to do with method `%s`", request.method)); + LOG.warning(String.format("Don't know what to do with method `%s`", r.method)); } } catch (Exception e) { LOG.log(Level.SEVERE, e.getMessage(), e); diff --git a/src/main/java/org/javacs/lsp/LanguageServer.java b/src/main/java/org/javacs/lsp/LanguageServer.java index e969c2e..c46808f 100644 --- a/src/main/java/org/javacs/lsp/LanguageServer.java +++ b/src/main/java/org/javacs/lsp/LanguageServer.java @@ -99,4 +99,8 @@ public class LanguageServer { public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { throw new RuntimeException("Unimplemented"); } + + public List<TextEdit> formatting(DocumentFormattingParams params) { + throw new RuntimeException("Unimplemented"); + } } diff --git a/src/main/java/org/javacs/lsp/Location.java b/src/main/java/org/javacs/lsp/Location.java index b35bf82..8484c78 100644 --- a/src/main/java/org/javacs/lsp/Location.java +++ b/src/main/java/org/javacs/lsp/Location.java @@ -3,6 +3,13 @@ package org.javacs.lsp; import java.net.URI; public class Location { - URI uri; - Range range; + public URI uri; + public Range range; + + public Location() {} + + public Location(URI uri, Range range) { + this.uri = uri; + this.range = range; + } } diff --git a/src/main/java/org/javacs/lsp/MarkedString.java b/src/main/java/org/javacs/lsp/MarkedString.java index da0f33f..d042d78 100644 --- a/src/main/java/org/javacs/lsp/MarkedString.java +++ b/src/main/java/org/javacs/lsp/MarkedString.java @@ -2,4 +2,11 @@ package org.javacs.lsp; public class MarkedString { public String language, value; + + public MarkedString() {} + + public MarkedString(String language, String value) { + this.language = language; + this.value = value; + } } diff --git a/src/main/java/org/javacs/lsp/MarkupContent.java b/src/main/java/org/javacs/lsp/MarkupContent.java index 3e2d58f..11b4f43 100644 --- a/src/main/java/org/javacs/lsp/MarkupContent.java +++ b/src/main/java/org/javacs/lsp/MarkupContent.java @@ -1,6 +1,12 @@ package org.javacs.lsp; public class MarkupContent { - public MarkupKind kind; - public String value; + public String kind, value; + + public MarkupContent() {} + + public MarkupContent(String kind, String value) { + this.kind = kind; + this.value = value; + } } diff --git a/src/main/java/org/javacs/lsp/Position.java b/src/main/java/org/javacs/lsp/Position.java index ae6b466..6c1fa40 100644 --- a/src/main/java/org/javacs/lsp/Position.java +++ b/src/main/java/org/javacs/lsp/Position.java @@ -3,4 +3,11 @@ package org.javacs.lsp; public class Position { // 0-based public int line, character; + + public Position() {} + + public Position(int line, int character) { + this.line = line; + this.character = character; + } } diff --git a/src/main/java/org/javacs/lsp/PublishDiagnosticsParams.java b/src/main/java/org/javacs/lsp/PublishDiagnosticsParams.java index 8e19d05..f2502c9 100644 --- a/src/main/java/org/javacs/lsp/PublishDiagnosticsParams.java +++ b/src/main/java/org/javacs/lsp/PublishDiagnosticsParams.java @@ -6,4 +6,11 @@ import java.util.List; public class PublishDiagnosticsParams { public URI uri; public List<Diagnostic> diagnostics; + + public PublishDiagnosticsParams() {} + + public PublishDiagnosticsParams(URI uri, List<Diagnostic> diagnostics) { + this.uri = uri; + this.diagnostics = diagnostics; + } } diff --git a/src/main/java/org/javacs/lsp/Range.java b/src/main/java/org/javacs/lsp/Range.java index 0d0e3c0..95bb123 100644 --- a/src/main/java/org/javacs/lsp/Range.java +++ b/src/main/java/org/javacs/lsp/Range.java @@ -2,4 +2,11 @@ package org.javacs.lsp; public class Range { public Position start, end; + + public Range() {} + + public Range(Position start, Position end) { + this.start = start; + this.end = end; + } } diff --git a/src/main/java/org/javacs/lsp/SignatureHelp.java b/src/main/java/org/javacs/lsp/SignatureHelp.java index eb25037..15fe366 100644 --- a/src/main/java/org/javacs/lsp/SignatureHelp.java +++ b/src/main/java/org/javacs/lsp/SignatureHelp.java @@ -4,6 +4,13 @@ import java.util.List; public class SignatureHelp { public List<SignatureInformation> signatures; - public Integer activeSignature; - public Integer activeParameter; + public Integer activeSignature, activeParameter; + + public SignatureHelp() {} + + public SignatureHelp(List<SignatureInformation> signatures, Integer activeSignature, Integer activeParameter) { + this.signatures = signatures; + this.activeSignature = activeSignature; + this.activeParameter = activeParameter; + } } diff --git a/src/main/java/org/javacs/lsp/TextDocumentIdentifier.java b/src/main/java/org/javacs/lsp/TextDocumentIdentifier.java index dce19b8..15f99dd 100644 --- a/src/main/java/org/javacs/lsp/TextDocumentIdentifier.java +++ b/src/main/java/org/javacs/lsp/TextDocumentIdentifier.java @@ -4,4 +4,10 @@ import java.net.URI; public class TextDocumentIdentifier { public URI uri; + + public TextDocumentIdentifier() {} + + public TextDocumentIdentifier(URI uri) { + this.uri = uri; + } } diff --git a/src/main/java/org/javacs/lsp/TextDocumentPositionParams.java b/src/main/java/org/javacs/lsp/TextDocumentPositionParams.java index 1d719cb..0a20afa 100644 --- a/src/main/java/org/javacs/lsp/TextDocumentPositionParams.java +++ b/src/main/java/org/javacs/lsp/TextDocumentPositionParams.java @@ -3,4 +3,11 @@ package org.javacs.lsp; public class TextDocumentPositionParams { public TextDocumentIdentifier textDocument; public Position position; + + public TextDocumentPositionParams() {} + + public TextDocumentPositionParams(TextDocumentIdentifier textDocument, Position position) { + this.textDocument = textDocument; + this.position = position; + } } diff --git a/src/main/java/org/javacs/lsp/TextEdit.java b/src/main/java/org/javacs/lsp/TextEdit.java index 03fe4e5..9278adb 100644 --- a/src/main/java/org/javacs/lsp/TextEdit.java +++ b/src/main/java/org/javacs/lsp/TextEdit.java @@ -3,4 +3,11 @@ package org.javacs.lsp; public class TextEdit { public Range range; public String newText; + + public TextEdit() {} + + public TextEdit(Range range, String newText) { + this.range = range; + this.newText = newText; + } } diff --git a/src/main/java/org/javacs/lsp/WorkspaceSymbolParams.java b/src/main/java/org/javacs/lsp/WorkspaceSymbolParams.java index d3859f9..eea3c4a 100644 --- a/src/main/java/org/javacs/lsp/WorkspaceSymbolParams.java +++ b/src/main/java/org/javacs/lsp/WorkspaceSymbolParams.java @@ -2,4 +2,10 @@ package org.javacs.lsp; public class WorkspaceSymbolParams { public String query; + + public WorkspaceSymbolParams() {} + + public WorkspaceSymbolParams(String query) { + this.query = query; + } } diff --git a/src/test/java/org/javacs/CodeLensTest.java b/src/test/java/org/javacs/CodeLensTest.java index 47f265d..29935c5 100644 --- a/src/test/java/org/javacs/CodeLensTest.java +++ b/src/test/java/org/javacs/CodeLensTest.java @@ -6,9 +6,6 @@ import static org.junit.Assert.*; import com.google.gson.Gson; import java.util.ArrayList; import java.util.List; -import java.util.Objects; -import java.util.StringJoiner; -import java.util.concurrent.ExecutionException; import org.javacs.lsp.*; import org.junit.Test; @@ -18,34 +15,24 @@ public class CodeLensTest { private List<? extends CodeLens> lenses(String file) { var uri = FindResource.uri(file); - var params = new CodeLensParams(new TextDocumentIdentifier(uri.toString())); - try { - var lenses = server.getTextDocumentService().codeLens(params).get(); - var resolved = new ArrayList<CodeLens>(); - for (var lens : lenses) { - if (lens.getCommand() == null) { - var gson = new Gson(); - var data = lens.getData(); - var dataJson = gson.toJsonTree(data); - lens.setData(dataJson); - lens = server.getTextDocumentService().resolveCodeLens(lens).get(); - } - resolved.add(lens); + var params = new CodeLensParams(new TextDocumentIdentifier(uri)); + var lenses = server.codeLens(params); + var resolved = new ArrayList<CodeLens>(); + for (var lens : lenses) { + if (lens.command == null) { + var gson = new Gson(); + var data = lens.data; + lens = server.resolveCodeLens(lens); } - return resolved; - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); + resolved.add(lens); } + return resolved; } private List<String> commands(List<? extends CodeLens> lenses) { var commands = new ArrayList<String>(); for (var lens : lenses) { - var command = new StringJoiner(", "); - for (var arg : lens.getCommand().getArguments()) { - command.add(Objects.toString(arg)); - } - commands.add(command.toString()); + commands.add(String.format("%s(%s)", lens.command.command, lens.command.arguments)); } return commands; } @@ -53,8 +40,8 @@ public class CodeLensTest { private List<String> titles(List<? extends CodeLens> lenses) { var titles = new ArrayList<String>(); for (var lens : lenses) { - var line = lens.getRange().getStart().getLine() + 1; - var title = lens.getCommand().getTitle(); + var line = lens.range.start.line + 1; + var title = lens.command.title; titles.add(line + ":" + title); } return titles; @@ -66,9 +53,9 @@ public class CodeLensTest { assertThat(lenses, not(empty())); var commands = commands(lenses); - assertThat(commands, hasItem(containsString("HasTest, null"))); - assertThat(commands, hasItem(containsString("HasTest, testMethod"))); - assertThat(commands, hasItem(containsString("HasTest, otherTestMethod"))); + assertThat(commands, hasItem(containsString("\"HasTest\",null"))); + assertThat(commands, hasItem(containsString("\"HasTest\",\"testMethod\""))); + assertThat(commands, hasItem(containsString("\"HasTest\",\"otherTestMethod\""))); } @Test diff --git a/src/test/java/org/javacs/CompletionsBase.java b/src/test/java/org/javacs/CompletionsBase.java index 8ad10a8..30af123 100644 --- a/src/test/java/org/javacs/CompletionsBase.java +++ b/src/test/java/org/javacs/CompletionsBase.java @@ -2,7 +2,6 @@ package org.javacs; import com.google.gson.Gson; import java.util.*; -import java.util.concurrent.ExecutionException; import java.util.logging.Logger; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -18,9 +17,9 @@ public class CompletionsBase { } static String itemInsertTemplate(CompletionItem i) { - var text = i.getInsertText(); + var text = i.insertText; - if (text == null) text = i.getLabel(); + if (text == null) text = i.label; assert text != null : "Either insertText or label must be defined"; @@ -37,9 +36,9 @@ public class CompletionsBase { var items = items(file, row, column); var result = new HashSet<String>(); for (var i : items) { - i.setData(new Gson().toJsonTree(i.getData())); + i.data = new Gson().toJsonTree(i.data); var resolved = resolve(i); - result.add(resolved.getDetail()); + result.add(resolved.detail); } return result; } @@ -59,9 +58,9 @@ public class CompletionsBase { } static String itemInsertText(CompletionItem i) { - var text = i.getInsertText(); + var text = i.insertText; - if (text == null) text = i.getLabel(); + if (text == null) text = i.label; assert text != null : "Either insertText or label must be defined"; @@ -76,8 +75,7 @@ public class CompletionsBase { return items.stream() .flatMap( i -> { - if (i.getDocumentation() != null) - return Stream.of(i.getDocumentation().getRight().getValue().trim()); + if (i.documentation != null) return Stream.of(i.documentation.value.trim()); else return Stream.empty(); }) .collect(Collectors.toSet()); @@ -88,20 +86,12 @@ public class CompletionsBase { protected List<? extends CompletionItem> items(String file, int row, int column) { var uri = FindResource.uri(file); var position = - new CompletionParams(new TextDocumentIdentifier(uri.toString()), new Position(row - 1, column - 1)); + new TextDocumentPositionParams(new TextDocumentIdentifier(uri), new Position(row - 1, column - 1)); - try { - return server.getTextDocumentService().completion(position).get().getRight().getItems(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + return server.completion(position).get().items; } protected CompletionItem resolve(CompletionItem item) { - try { - return server.getTextDocumentService().resolveCompletionItem(item).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + return server.resolveCompletionItem(item); } } diff --git a/src/test/java/org/javacs/CompletionsTest.java b/src/test/java/org/javacs/CompletionsTest.java index 864ff24..d213a74 100644 --- a/src/test/java/org/javacs/CompletionsTest.java +++ b/src/test/java/org/javacs/CompletionsTest.java @@ -215,8 +215,8 @@ public class CompletionsTest extends CompletionsBase { } private static String sortText(CompletionItem i) { - if (i.getSortText() != null) return i.getSortText(); - else return i.getLabel(); + if (i.sortText != null) return i.sortText; + else return i.label; } @Test @@ -288,9 +288,9 @@ public class CompletionsTest extends CompletionsBase { var items = items(file, 9, 17); for (var item : items) { - if ("ArrayList".equals(item.getLabel())) { - assertThat(item.getAdditionalTextEdits(), not(nullValue())); - assertThat(item.getAdditionalTextEdits(), not(empty())); + if ("ArrayList".equals(item.label)) { + assertThat(item.additionalTextEdits, not(nullValue())); + assertThat(item.additionalTextEdits, not(empty())); return; } @@ -308,8 +308,8 @@ public class CompletionsTest extends CompletionsBase { var items = items(file, 6, 10); for (var item : items) { - if ("AutocompleteMember".equals(item.getLabel())) { - assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); + if ("AutocompleteMember".equals(item.label)) { + assertThat(item.additionalTextEdits, either(empty()).or(nullValue())); return; } @@ -327,8 +327,8 @@ public class CompletionsTest extends CompletionsBase { var items = items(file, 11, 38); for (var item : items) { - if ("ArrayIndexOutOfBoundsException".equals(item.getLabel())) { - assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); + if ("ArrayIndexOutOfBoundsException".equals(item.label)) { + assertThat(item.additionalTextEdits, either(empty()).or(nullValue())); return; } @@ -346,8 +346,8 @@ public class CompletionsTest extends CompletionsBase { var items = items(file, 6, 10); for (var item : items) { - if ("AutocompleteOther".equals(item.getLabel())) { - assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); + if ("AutocompleteOther".equals(item.label)) { + assertThat(item.additionalTextEdits, either(empty()).or(nullValue())); return; } @@ -365,8 +365,8 @@ public class CompletionsTest extends CompletionsBase { var items = items(file, 12, 14); for (var item : items) { - if ("Arrays".equals(item.getLabel())) { - assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); + if ("Arrays".equals(item.label)) { + assertThat(item.additionalTextEdits, either(empty()).or(nullValue())); return; } @@ -384,8 +384,8 @@ public class CompletionsTest extends CompletionsBase { var items = items(file, 10, 26); for (var item : items) { - if ("ArrayBlockingQueue".equals(item.getLabel())) { - assertThat(item.getAdditionalTextEdits(), either(empty()).or(nullValue())); + if ("ArrayBlockingQueue".equals(item.label)) { + assertThat(item.additionalTextEdits, either(empty()).or(nullValue())); return; } @@ -400,7 +400,7 @@ public class CompletionsTest extends CompletionsBase { // Static methods var items = items(file, 8, 17); - var suggestions = items.stream().map(i -> i.getLabel()).collect(Collectors.toSet()); + var suggestions = items.stream().map(i -> i.label).collect(Collectors.toSet()); assertThat(suggestions, hasItems("add", "addAll")); } @@ -476,8 +476,8 @@ public class CompletionsTest extends CompletionsBase { // Static methods var items = items(file, 5, 18); - var suggestions = items.stream().map(i -> i.getLabel()).collect(Collectors.toSet()); - var details = items.stream().map(i -> i.getDetail()).collect(Collectors.toSet()); + var suggestions = items.stream().map(i -> i.label).collect(Collectors.toSet()); + var details = items.stream().map(i -> i.detail).collect(Collectors.toSet()); assertThat(suggestions, hasItems("restMethod")); assertThat(details, hasItems("void (String... params)")); @@ -501,14 +501,13 @@ public class CompletionsTest extends CompletionsBase { // Static methods var items = items(file, 6, 19); - var suggestions = Lists.transform(items, i -> i.getInsertText()); + var suggestions = Lists.transform(items, i -> i.insertText); assertThat(suggestions, hasItems("ArrayList<>($0)")); for (var each : items) { - if (each.getInsertText().equals("ArrayList<>")) - assertThat( - "new ? auto-imports", each.getAdditionalTextEdits(), both(not(empty())).and(not(nullValue()))); + if (each.insertText.equals("ArrayList<>")) + assertThat("new ? auto-imports", each.additionalTextEdits, both(not(empty())).and(not(nullValue()))); } } @@ -557,17 +556,17 @@ public class CompletionsTest extends CompletionsBase { // Static methods var items = items(file, 4, 25); - var suggestions = Lists.transform(items, i -> i.getLabel()); + var suggestions = Lists.transform(items, i -> i.label); assertThat(suggestions, hasItems("OtherPackagePublic")); assertThat(suggestions, not(hasItems("OtherPackagePrivate"))); // Imports are now being managed by FixImports // for (var item : items) { - // if (item.getLabel().equals("OtherPackagePublic")) + // if (item.label.equals("OtherPackagePublic")) // assertThat( // "Don't import when completing imports", - // item.getAdditionalTextEdits(), + // item.additionalTextEdits, // either(empty()).or(nullValue())); // } } @@ -578,14 +577,14 @@ public class CompletionsTest extends CompletionsBase { // Static methods var items = items(file, 5, 14); - var suggestions = Lists.transform(items, i -> i.getLabel()); + var suggestions = Lists.transform(items, i -> i.label); assertThat(suggestions, hasItems("OtherPackagePublic")); assertThat(suggestions, not(hasItems("OtherPackagePrivate"))); // for (var item : items) { - // if (item.getLabel().equals("OtherPackagePublic")) - // assertThat("Auto-import OtherPackagePublic", item.getAdditionalTextEdits(), not(empty())); + // if (item.label.equals("OtherPackagePublic")) + // assertThat("Auto-import OtherPackagePublic", item.additionalTextEdits, not(empty())); // } } diff --git a/src/test/java/org/javacs/FindReferencesTest.java b/src/test/java/org/javacs/FindReferencesTest.java index 9356ded..e8d51c3 100644 --- a/src/test/java/org/javacs/FindReferencesTest.java +++ b/src/test/java/org/javacs/FindReferencesTest.java @@ -3,10 +3,8 @@ package org.javacs; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import java.net.URI; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.logging.Logger; import org.javacs.lsp.*; import org.junit.Test; @@ -20,22 +18,17 @@ public class FindReferencesTest { var uri = FindResource.uri(file); var params = new ReferenceParams(); - params.setTextDocument(new TextDocumentIdentifier(uri.toString())); - params.setUri(uri.toString()); - params.setPosition(new Position(row - 1, column - 1)); - - try { - var locations = server.getTextDocumentService().references(params).get(); - var strings = new ArrayList<String>(); - for (var l : locations) { - var fileName = Parser.fileName(URI.create(l.getUri())); - var line = l.getRange().getStart().getLine(); - strings.add(String.format("%s(%d)", fileName, line + 1)); - } - return strings; - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); + params.textDocument = new TextDocumentIdentifier(uri); + params.position = new Position(row - 1, column - 1); + + var locations = server.findReferences(params); + var strings = new ArrayList<String>(); + for (var l : locations) { + var fileName = Parser.fileName(l.uri); + var line = l.range.start.line; + strings.add(String.format("%s(%d)", fileName, line + 1)); } + return strings; } @Test diff --git a/src/test/java/org/javacs/GotoTest.java b/src/test/java/org/javacs/GotoTest.java index ac60efc..74bc9dd 100644 --- a/src/test/java/org/javacs/GotoTest.java +++ b/src/test/java/org/javacs/GotoTest.java @@ -3,11 +3,9 @@ package org.javacs; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; -import java.net.URI; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; -import java.util.concurrent.ExecutionException; import org.javacs.lsp.*; import org.junit.Ignore; import org.junit.Test; @@ -128,30 +126,24 @@ public class GotoTest { private List<String> doGoto(String file, int row, int column) { TextDocumentIdentifier document = new TextDocumentIdentifier(); - document.setUri(FindResource.uri(file).toString()); + document.uri = FindResource.uri(file); Position position = new Position(); - position.setLine(row); - position.setCharacter(column); + position.line = row; + position.character = column; TextDocumentPositionParams p = new TextDocumentPositionParams(); - p.setTextDocument(document); - p.setPosition(position); + p.textDocument = document; + p.position = position; - // TODO extends is not coloring correctly - List<? extends Location> locations; - try { - locations = server.getTextDocumentService().definition(p).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + var locations = server.gotoDefinition(p); var strings = new ArrayList<String>(); for (var l : locations) { - var fileName = Paths.get(URI.create(l.getUri())).getFileName(); - var start = l.getRange().getStart(); - strings.add(String.format("%s:%d", fileName, start.getLine() + 1)); + var fileName = Paths.get(l.uri).getFileName(); + var start = l.range.start; + strings.add(String.format("%s:%d", fileName, start.line + 1)); } return strings; } diff --git a/src/test/java/org/javacs/JavaCompilerServiceTest.java b/src/test/java/org/javacs/JavaCompilerServiceTest.java index ffcbc26..7b8d20c 100644 --- a/src/test/java/org/javacs/JavaCompilerServiceTest.java +++ b/src/test/java/org/javacs/JavaCompilerServiceTest.java @@ -244,7 +244,7 @@ public class JavaCompilerServiceTest { var uri = r.getCompilationUnit().getSourceFile().toUri(); var fileName = Paths.get(uri).getFileName(); var range = batch.range(r).get(); - stringify.add(String.format("%s:%d", fileName, range.getStart().getLine() + 1)); + stringify.add(String.format("%s:%d", fileName, range.start.line + 1)); } assertThat(stringify, hasItem("GotoDefinition.java:3")); assertThat(stringify, not(hasItem("GotoDefinition.java:6"))); diff --git a/src/test/java/org/javacs/LanguageServerFixture.java b/src/test/java/org/javacs/LanguageServerFixture.java index 6f0a481..2c93283 100644 --- a/src/test/java/org/javacs/LanguageServerFixture.java +++ b/src/test/java/org/javacs/LanguageServerFixture.java @@ -1,8 +1,8 @@ package org.javacs; +import com.google.gson.JsonElement; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; import java.util.logging.Logger; import org.javacs.lsp.*; @@ -16,53 +16,32 @@ class LanguageServerFixture { } static JavaLanguageServer getJavaLanguageServer() { - return getJavaLanguageServer(DEFAULT_WORKSPACE_ROOT, diagnostic -> LOG.info(diagnostic.getMessage())); + return getJavaLanguageServer(DEFAULT_WORKSPACE_ROOT, diagnostic -> LOG.info(diagnostic.message)); } static JavaLanguageServer getJavaLanguageServer(Path workspaceRoot, Consumer<Diagnostic> onError) { return getJavaLanguageServer( workspaceRoot, - new CustomLanguageClient() { - @Override - public void telemetryEvent(Object o) {} - - @Override - public void publishDiagnostics(PublishDiagnosticsParams publishDiagnosticsParams) { - publishDiagnosticsParams.getDiagnostics().forEach(onError); - } - - @Override - public void showMessage(MessageParams messageParams) {} - - @Override - public CompletableFuture<MessageActionItem> showMessageRequest( - ShowMessageRequestParams showMessageRequestParams) { - return null; + new LanguageClient() { + public void publishDiagnostics(PublishDiagnosticsParams params) { + params.diagnostics.forEach(onError); } - @Override - public void logMessage(MessageParams messageParams) {} - - @Override - public void javaStartProgress(JavaStartProgressParams params) {} + public void showMessage(ShowMessageParams params) {} - @Override - public void javaReportProgress(JavaReportProgressParams params) {} + public void registerCapability(String method, JsonElement options) {} - @Override - public void javaEndProgress() {} + public void customNotification(String method, JsonElement params) {} }); } - private static JavaLanguageServer getJavaLanguageServer(Path workspaceRoot, CustomLanguageClient client) { - var server = new JavaLanguageServer(); + private static JavaLanguageServer getJavaLanguageServer(Path workspaceRoot, LanguageClient client) { + var server = new JavaLanguageServer(client); var init = new InitializeParams(); - init.setRootUri(workspaceRoot.toUri().toString()); - - server.installClient(client); + init.rootUri = workspaceRoot.toUri(); server.initialize(init); - server.initialized(null); + server.initialized(); return server; } diff --git a/src/test/java/org/javacs/SearchTest.java b/src/test/java/org/javacs/SearchTest.java index ace86e8..3ecc567 100644 --- a/src/test/java/org/javacs/SearchTest.java +++ b/src/test/java/org/javacs/SearchTest.java @@ -9,7 +9,6 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Set; -import java.util.concurrent.ExecutionException; import java.util.logging.Logger; import java.util.stream.Collectors; import org.javacs.lsp.*; @@ -27,37 +26,25 @@ public class SearchTest { var textContent = Joiner.on("\n").join(Files.readAllLines(Paths.get(uri))); var document = new TextDocumentItem(); - document.setUri(uri.toString()); - document.setText(textContent); + document.uri = uri; + document.text = textContent; - server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(document, null)); + server.didOpenTextDocument(new DidOpenTextDocumentParams(document)); } private static Set<String> searchWorkspace(String query, int limit) { - try { - return server.getWorkspaceService() - .symbol(new WorkspaceSymbolParams(query)) - .get() - .stream() - .map(result -> result.getName()) - .limit(limit) - .collect(Collectors.toSet()); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + return server.workspaceSymbols(new WorkspaceSymbolParams(query)) + .stream() + .map(result -> result.name) + .limit(limit) + .collect(Collectors.toSet()); } private static Set<String> searchFile(URI uri) { - try { - return server.getTextDocumentService() - .documentSymbol(new DocumentSymbolParams(new TextDocumentIdentifier(uri.toString()))) - .get() - .stream() - .map(result -> result.getLeft().getName()) - .collect(Collectors.toSet()); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + return server.documentSymbol(new DocumentSymbolParams(new TextDocumentIdentifier(uri))) + .stream() + .map(result -> result.name) + .collect(Collectors.toSet()); } @Test diff --git a/src/test/java/org/javacs/SignatureHelpTest.java b/src/test/java/org/javacs/SignatureHelpTest.java index 6019021..ec05577 100644 --- a/src/test/java/org/javacs/SignatureHelpTest.java +++ b/src/test/java/org/javacs/SignatureHelpTest.java @@ -4,7 +4,6 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.io.IOException; -import java.util.concurrent.ExecutionException; import org.javacs.lsp.*; import org.junit.Test; @@ -13,34 +12,34 @@ public class SignatureHelpTest { public void signatureHelp() throws IOException { var help = doHelp("/org/javacs/example/SignatureHelp.java", 7, 36); - assertThat(help.getSignatures(), hasSize(2)); + assertThat(help.signatures, hasSize(2)); } @Test public void partlyFilledIn() throws IOException { var help = doHelp("/org/javacs/example/SignatureHelp.java", 8, 39); - assertThat(help.getSignatures(), hasSize(2)); - assertThat(help.getActiveSignature(), equalTo(1)); - assertThat(help.getActiveParameter(), equalTo(1)); + assertThat(help.signatures, hasSize(2)); + assertThat(help.activeSignature, equalTo(1)); + assertThat(help.activeParameter, equalTo(1)); } @Test public void constructor() throws IOException { var help = doHelp("/org/javacs/example/SignatureHelp.java", 9, 27); - assertThat(help.getSignatures(), hasSize(1)); - assertThat(help.getSignatures().get(0).getLabel(), startsWith("SignatureHelp")); + assertThat(help.signatures, hasSize(1)); + assertThat(help.signatures.get(0).label, startsWith("SignatureHelp")); } @Test public void platformConstructor() throws IOException { var help = doHelp("/org/javacs/example/SignatureHelp.java", 10, 26); - assertThat(help.getSignatures(), not(empty())); - assertThat(help.getSignatures().get(0).getLabel(), startsWith("ArrayList")); + assertThat(help.signatures, not(empty())); + assertThat(help.signatures.get(0).label, startsWith("ArrayList")); // TODO - // assertThat(help.getSignatures().get(0).getDocumentation(), not(nullValue())); + // assertThat(help.signatures.get(0).documentation, not(nullValue())); } private static final JavaLanguageServer server = LanguageServerFixture.getJavaLanguageServer(); @@ -48,22 +47,18 @@ public class SignatureHelpTest { private SignatureHelp doHelp(String file, int row, int column) throws IOException { var document = new TextDocumentIdentifier(); - document.setUri(FindResource.uri(file).toString()); + document.uri = FindResource.uri(file); var position = new Position(); - position.setLine(row - 1); - position.setCharacter(column - 1); + position.line = row - 1; + position.character = column - 1; var p = new TextDocumentPositionParams(); - p.setTextDocument(document); - p.setPosition(position); + p.textDocument = document; + p.position = position; - try { - return server.getTextDocumentService().signatureHelp(p).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); - } + return server.signatureHelp(p).get(); } } diff --git a/src/test/java/org/javacs/SymbolUnderCursorTest.java b/src/test/java/org/javacs/SymbolUnderCursorTest.java index ca04883..a606f5d 100644 --- a/src/test/java/org/javacs/SymbolUnderCursorTest.java +++ b/src/test/java/org/javacs/SymbolUnderCursorTest.java @@ -4,7 +4,6 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.util.StringJoiner; -import java.util.concurrent.ExecutionException; import org.javacs.lsp.*; import org.junit.Ignore; import org.junit.Test; @@ -108,18 +107,10 @@ public class SymbolUnderCursorTest { private String symbolAt(String file, int line, int character) { var pos = new TextDocumentPositionParams( - new TextDocumentIdentifier(FindResource.uri(file).toString()), - new Position(line - 1, character - 1)); + new TextDocumentIdentifier(FindResource.uri(file)), new Position(line - 1, character - 1)); var result = new StringJoiner("\n"); - try { - server.getTextDocumentService() - .hover(pos) - .get() - .getContents() - .getLeft() - .forEach(hover -> result.add(hover.isLeft() ? hover.getLeft() : hover.getRight().getValue())); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException(e); + for (var h : server.hover(pos).get().contents) { + result.add(h.value); } return result.toString(); } diff --git a/src/test/java/org/javacs/lsp/LanguageServerTest.java b/src/test/java/org/javacs/lsp/LanguageServerTest.java index 75a1faf..4330403 100644 --- a/src/test/java/org/javacs/lsp/LanguageServerTest.java +++ b/src/test/java/org/javacs/lsp/LanguageServerTest.java @@ -65,10 +65,24 @@ public class LanguageServerTest { throws IOException, InterruptedException, ExecutionException, TimeoutException { // Send initialize message and wait for ack sendToServer(initializeMessage); - receivedInitialize.get(1, TimeUnit.SECONDS); + receivedInitialize.get(10, TimeUnit.SECONDS); // Send exit message and wait for exit sendToServer(exitMessage); - main.join(1000); + main.join(10_000); + assertThat("Main thread has quit", main.isAlive(), equalTo(false)); + } + + @Test + public void endOfStreamKillsServer() + throws IOException, InterruptedException, ExecutionException, TimeoutException { + // Send initialize message and wait for ack + sendToServer(initializeMessage); + receivedInitialize.get(10, TimeUnit.SECONDS); + // Close stream + writeClientToServer.close(); + clientToServer.close(); + // Wait for exit + main.join(10_000); assertThat("Main thread has quit", main.isAlive(), equalTo(false)); } } diff --git a/src/test/java/org/javacs/lsp/LspTest.java b/src/test/java/org/javacs/lsp/LspTest.java index 3f65774..9ef7c4b 100644 --- a/src/test/java/org/javacs/lsp/LspTest.java +++ b/src/test/java/org/javacs/lsp/LspTest.java @@ -8,6 +8,7 @@ import com.google.gson.JsonObject; import java.io.IOException; import java.io.PipedInputStream; import java.io.PipedOutputStream; +import java.util.Optional; import org.junit.Before; import org.junit.Test; @@ -47,6 +48,20 @@ public class LspTest { } @Test + public void writeOptional() { + LSP.respond(writer, 1, Optional.of(1)); + var expected = "Content-Length: 35\r\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":1}"; + assertThat(bufferToString(), equalTo(expected)); + } + + @Test + public void writeEmpty() { + LSP.respond(writer, 1, Optional.empty()); + var expected = "Content-Length: 38\r\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":null}"; + assertThat(bufferToString(), equalTo(expected)); + } + + @Test public void readMessage() throws IOException { var message = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}"; var header = String.format("Content-Length: %d\r\n\r\n", message.getBytes().length); |