diff options
author | George Fraser <george@fivetran.com> | 2018-12-28 12:31:18 -0800 |
---|---|---|
committer | George Fraser <george@fivetran.com> | 2018-12-28 12:31:18 -0800 |
commit | 1f1267e0c7d89750dbd6ff8806a73f8cfd9e673e (patch) | |
tree | 01f65dfc24489fce38f3d18c3f751b13a851ccbd /src | |
parent | f19f80b3bdfc99f2062710f3d2193c8a16f26ecd (diff) | |
download | java-language-server-1f1267e0c7d89750dbd6ff8806a73f8cfd9e673e.zip |
Minimalist LSP implementation
Diffstat (limited to 'src')
80 files changed, 1223 insertions, 1 deletions
diff --git a/src/main/java/org/javacs/JavaTextDocumentService.java b/src/main/java/org/javacs/JavaTextDocumentService.java index 6707cd0..a1cc285 100644 --- a/src/main/java/org/javacs/JavaTextDocumentService.java +++ b/src/main/java/org/javacs/JavaTextDocumentService.java @@ -326,7 +326,7 @@ class JavaTextDocumentService implements TextDocumentService { var line = position.getPosition().getLine() + 1; var column = position.getPosition().getCharacter() + 1; var el = hoverCache.element(line, column); - if (el.isEmpty()) return CompletableFuture.completedFuture(new Hover(Collections.emptyList())); + if (!el.isPresent()) return CompletableFuture.completedFuture(new Hover(Collections.emptyList())); // Add code hover message var result = new ArrayList<Either<String, MarkedString>>(); diff --git a/src/main/java/org/javacs/lsp/CancelParams.java b/src/main/java/org/javacs/lsp/CancelParams.java new file mode 100644 index 0000000..cf7acce --- /dev/null +++ b/src/main/java/org/javacs/lsp/CancelParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class CancelParams { + public String id; +} diff --git a/src/main/java/org/javacs/lsp/CodeAction.java b/src/main/java/org/javacs/lsp/CodeAction.java new file mode 100644 index 0000000..b07a5a9 --- /dev/null +++ b/src/main/java/org/javacs/lsp/CodeAction.java @@ -0,0 +1,10 @@ +package org.javacs.lsp; + +import java.util.List; + +public class CodeAction { + public String title, kind; + public List<Diagnostic> diagnostics; + public WorkspaceEdit edit; + public Command command; +} diff --git a/src/main/java/org/javacs/lsp/CodeActionContext.java b/src/main/java/org/javacs/lsp/CodeActionContext.java new file mode 100644 index 0000000..7ded497 --- /dev/null +++ b/src/main/java/org/javacs/lsp/CodeActionContext.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.util.List; + +public class CodeActionContext { + public List<Diagnostic> diagnostics; + public List<CodeActionKind> only; +} diff --git a/src/main/java/org/javacs/lsp/CodeActionKind.java b/src/main/java/org/javacs/lsp/CodeActionKind.java new file mode 100644 index 0000000..4495545 --- /dev/null +++ b/src/main/java/org/javacs/lsp/CodeActionKind.java @@ -0,0 +1,11 @@ +package org.javacs.lsp; + +public class CodeActionKind { + public static final String QuickFix = "quickfix", + Refactor = "refactor", + RefactorExtract = "refactor.extract", + RefactorInline = "refactor.inline", + RefactorRewrite = "refactor.rewrite", + Source = "source", + SourceOrganizeImports = "source.organizeImports"; +} diff --git a/src/main/java/org/javacs/lsp/CodeActionParams.java b/src/main/java/org/javacs/lsp/CodeActionParams.java new file mode 100644 index 0000000..77bb798 --- /dev/null +++ b/src/main/java/org/javacs/lsp/CodeActionParams.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +public class CodeActionParams { + public TextDocumentIdentifier textDocument; + public Range range; + public CodeActionContext context; +} diff --git a/src/main/java/org/javacs/lsp/CodeLens.java b/src/main/java/org/javacs/lsp/CodeLens.java new file mode 100644 index 0000000..79158a7 --- /dev/null +++ b/src/main/java/org/javacs/lsp/CodeLens.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +import com.google.gson.JsonArray; + +public class CodeLens { + public Range range; + public Command command; + public JsonArray data; +} diff --git a/src/main/java/org/javacs/lsp/CodeLensParams.java b/src/main/java/org/javacs/lsp/CodeLensParams.java new file mode 100644 index 0000000..7958ac7 --- /dev/null +++ b/src/main/java/org/javacs/lsp/CodeLensParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class CodeLensParams { + public TextDocumentIdentifier textDocument; +} diff --git a/src/main/java/org/javacs/lsp/Command.java b/src/main/java/org/javacs/lsp/Command.java new file mode 100644 index 0000000..a212c92 --- /dev/null +++ b/src/main/java/org/javacs/lsp/Command.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import com.google.gson.JsonArray; + +public class Command { + public String title, command; + public JsonArray arguments; +} diff --git a/src/main/java/org/javacs/lsp/CompletionContext.java b/src/main/java/org/javacs/lsp/CompletionContext.java new file mode 100644 index 0000000..f10547f --- /dev/null +++ b/src/main/java/org/javacs/lsp/CompletionContext.java @@ -0,0 +1,6 @@ +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 new file mode 100644 index 0000000..1a46753 --- /dev/null +++ b/src/main/java/org/javacs/lsp/CompletionItem.java @@ -0,0 +1,18 @@ +package org.javacs.lsp; + +import com.google.gson.JsonArray; +import java.util.List; + +public class CompletionItem { + public String label; + public int kind; + public String detail; + public MarkupContent documentation; + public boolean deprecated, preselect; + public String sortText, filterText, insertText, insertTextFormat; + public TextEdit textEdit; + public List<TextEdit> additionalTextEdits; + public List<Character> commitCharacters; + public Command command; + public JsonArray data; +} diff --git a/src/main/java/org/javacs/lsp/CompletionItemKind.java b/src/main/java/org/javacs/lsp/CompletionItemKind.java new file mode 100644 index 0000000..3a7d18c --- /dev/null +++ b/src/main/java/org/javacs/lsp/CompletionItemKind.java @@ -0,0 +1,29 @@ +package org.javacs.lsp; + +public class CompletionItemKind { + public static final int Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25; +} diff --git a/src/main/java/org/javacs/lsp/CompletionList.java b/src/main/java/org/javacs/lsp/CompletionList.java new file mode 100644 index 0000000..48d3155 --- /dev/null +++ b/src/main/java/org/javacs/lsp/CompletionList.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.util.List; + +public class CompletionList { + public boolean isIncomplete; + public List<CompletionItem> items; +} diff --git a/src/main/java/org/javacs/lsp/CompletionParams.java b/src/main/java/org/javacs/lsp/CompletionParams.java new file mode 100644 index 0000000..c3dd2ef --- /dev/null +++ b/src/main/java/org/javacs/lsp/CompletionParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class CompletionParams extends TextDocumentPositionParams { + public CompletionContext context; +} diff --git a/src/main/java/org/javacs/lsp/CompletionTriggerKind.java b/src/main/java/org/javacs/lsp/CompletionTriggerKind.java new file mode 100644 index 0000000..caae0fb --- /dev/null +++ b/src/main/java/org/javacs/lsp/CompletionTriggerKind.java @@ -0,0 +1,18 @@ +package org.javacs.lsp; + +public class CompletionTriggerKind { + /** + * Completion was triggered by typing an identifier (24x7 code complete), manual invocation (e.g Ctrl+Space) or via + * API. + */ + public static final int Invoked = 1; + + /** + * Completion was triggered by a trigger character specified by the `triggerCharacters` properties of the + * `CompletionRegistrationOptions`. + */ + public static final int TriggerCharacter = 2; + + /** Completion was re-triggered as the current completion list is incomplete. */ + public static final int TriggerForIncompleteCompletions = 3; +} diff --git a/src/main/java/org/javacs/lsp/Diagnostic.java b/src/main/java/org/javacs/lsp/Diagnostic.java new file mode 100644 index 0000000..75986f7 --- /dev/null +++ b/src/main/java/org/javacs/lsp/Diagnostic.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +public class Diagnostic { + public Range range; + public int severity; + public String code, source, message; +} diff --git a/src/main/java/org/javacs/lsp/DiagnosticSeverity.java b/src/main/java/org/javacs/lsp/DiagnosticSeverity.java new file mode 100644 index 0000000..1b9a867 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DiagnosticSeverity.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class DiagnosticSeverity { + public static final int Error = 1, Warning = 2, Information = 3, Hint = 4; +} diff --git a/src/main/java/org/javacs/lsp/DiagnosticTag.java b/src/main/java/org/javacs/lsp/DiagnosticTag.java new file mode 100644 index 0000000..09aba74 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DiagnosticTag.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class DiagnosticTag { + public static final int Unnecessary = 1; +} diff --git a/src/main/java/org/javacs/lsp/DidChangeConfigurationParams.java b/src/main/java/org/javacs/lsp/DidChangeConfigurationParams.java new file mode 100644 index 0000000..8554ae1 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DidChangeConfigurationParams.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; + +public class DidChangeConfigurationParams { + public JsonElement settings; +} diff --git a/src/main/java/org/javacs/lsp/DidChangeTextDocumentParams.java b/src/main/java/org/javacs/lsp/DidChangeTextDocumentParams.java new file mode 100644 index 0000000..75b8dd3 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DidChangeTextDocumentParams.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.util.List; + +public class DidChangeTextDocumentParams { + public VersionedTextDocumentIdentifier textDocument; + public List<TextDocumentContentChangeEvent> contentChanges; +} diff --git a/src/main/java/org/javacs/lsp/DidChangeWatchedFilesParams.java b/src/main/java/org/javacs/lsp/DidChangeWatchedFilesParams.java new file mode 100644 index 0000000..23e525d --- /dev/null +++ b/src/main/java/org/javacs/lsp/DidChangeWatchedFilesParams.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +import java.util.List; + +public class DidChangeWatchedFilesParams { + public List<FileEvent> changes; +} diff --git a/src/main/java/org/javacs/lsp/DidChangeWorkspaceFoldersParams.java b/src/main/java/org/javacs/lsp/DidChangeWorkspaceFoldersParams.java new file mode 100644 index 0000000..742ee4a --- /dev/null +++ b/src/main/java/org/javacs/lsp/DidChangeWorkspaceFoldersParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class DidChangeWorkspaceFoldersParams { + public WorkspaceFoldersChangeEvent event; +} diff --git a/src/main/java/org/javacs/lsp/DidCloseTextDocumentParams.java b/src/main/java/org/javacs/lsp/DidCloseTextDocumentParams.java new file mode 100644 index 0000000..67dc5e0 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DidCloseTextDocumentParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class DidCloseTextDocumentParams { + public TextDocumentIdentifier textDocument; +} diff --git a/src/main/java/org/javacs/lsp/DidOpenTextDocument.java b/src/main/java/org/javacs/lsp/DidOpenTextDocument.java new file mode 100644 index 0000000..ccbcf4b --- /dev/null +++ b/src/main/java/org/javacs/lsp/DidOpenTextDocument.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class DidOpenTextDocument { + public TextDocumentItem textDocument; +} diff --git a/src/main/java/org/javacs/lsp/DidOpenTextDocumentParams.java b/src/main/java/org/javacs/lsp/DidOpenTextDocumentParams.java new file mode 100644 index 0000000..1083a95 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DidOpenTextDocumentParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class DidOpenTextDocumentParams { + public TextDocumentItem textDocument; +} diff --git a/src/main/java/org/javacs/lsp/DidSaveTextDocumentParams.java b/src/main/java/org/javacs/lsp/DidSaveTextDocumentParams.java new file mode 100644 index 0000000..6c38a53 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DidSaveTextDocumentParams.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class DidSaveTextDocumentParams { + public TextDocumentIdentifier textDocument; + public String text; +} diff --git a/src/main/java/org/javacs/lsp/DocumentHighlight.java b/src/main/java/org/javacs/lsp/DocumentHighlight.java new file mode 100644 index 0000000..b3bbf79 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DocumentHighlight.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class DocumentHighlight { + public Range range; + public int kind; +} diff --git a/src/main/java/org/javacs/lsp/DocumentHighlightKind.java b/src/main/java/org/javacs/lsp/DocumentHighlightKind.java new file mode 100644 index 0000000..1712c82 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DocumentHighlightKind.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class DocumentHighlightKind { + public static final int Text = 1, Read = 2, Write = 3; +} diff --git a/src/main/java/org/javacs/lsp/DocumentLink.java b/src/main/java/org/javacs/lsp/DocumentLink.java new file mode 100644 index 0000000..06877fa --- /dev/null +++ b/src/main/java/org/javacs/lsp/DocumentLink.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +import com.google.gson.JsonArray; + +public class DocumentLink { + public Range range; + public String target; + public JsonArray data; +} diff --git a/src/main/java/org/javacs/lsp/DocumentSymbol.java b/src/main/java/org/javacs/lsp/DocumentSymbol.java new file mode 100644 index 0000000..4510aa9 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DocumentSymbol.java @@ -0,0 +1,11 @@ +package org.javacs.lsp; + +import java.util.List; + +public class DocumentSymbol { + public String name, detail; + public int kind; + public boolean deprecated; + public Range range, selectionRange; + public List<DocumentSymbol> children; +} diff --git a/src/main/java/org/javacs/lsp/DocumentSymbolParams.java b/src/main/java/org/javacs/lsp/DocumentSymbolParams.java new file mode 100644 index 0000000..ff8e740 --- /dev/null +++ b/src/main/java/org/javacs/lsp/DocumentSymbolParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class DocumentSymbolParams { + public TextDocumentIdentifier textDocument; +} diff --git a/src/main/java/org/javacs/lsp/ErrorCodes.java b/src/main/java/org/javacs/lsp/ErrorCodes.java new file mode 100644 index 0000000..98b92f7 --- /dev/null +++ b/src/main/java/org/javacs/lsp/ErrorCodes.java @@ -0,0 +1,18 @@ +package org.javacs.lsp; + +public class ErrorCodes { + public static final int ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + serverErrorStart = -32099, + serverErrorEnd = -32000, + ServerNotInitialized = -32002, + UnknownErrorCode = -32001, + + // TODO comment doesn't highlight properly + // Defined by the protocol. + RequestCancelled = -32800, + ContentModified = -32801; +} diff --git a/src/main/java/org/javacs/lsp/FileChangeType.java b/src/main/java/org/javacs/lsp/FileChangeType.java new file mode 100644 index 0000000..076abcd --- /dev/null +++ b/src/main/java/org/javacs/lsp/FileChangeType.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class FileChangeType { + public static final int Created = 1, Changed = 2, Deleted = 3; +} diff --git a/src/main/java/org/javacs/lsp/FileEvent.java b/src/main/java/org/javacs/lsp/FileEvent.java new file mode 100644 index 0000000..2f766e3 --- /dev/null +++ b/src/main/java/org/javacs/lsp/FileEvent.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.net.URI; + +public class FileEvent { + public URI uri; + public int type; +} diff --git a/src/main/java/org/javacs/lsp/Hover.java b/src/main/java/org/javacs/lsp/Hover.java new file mode 100644 index 0000000..25dadd0 --- /dev/null +++ b/src/main/java/org/javacs/lsp/Hover.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.util.List; + +public class Hover { + public List<MarkedString> contents; + public Range range; +} diff --git a/src/main/java/org/javacs/lsp/InitializeParams.java b/src/main/java/org/javacs/lsp/InitializeParams.java new file mode 100644 index 0000000..5c86b8b --- /dev/null +++ b/src/main/java/org/javacs/lsp/InitializeParams.java @@ -0,0 +1,14 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; +import java.net.URI; +import java.util.List; + +public class InitializeParams { + public int processId; + public String rootPath; + public URI rootUri; + public JsonElement initializationOptions; + public String trace; + public List<WorkspaceFolder> workspaceFolders; +} diff --git a/src/main/java/org/javacs/lsp/InitializeResult.java b/src/main/java/org/javacs/lsp/InitializeResult.java new file mode 100644 index 0000000..a8c8cee --- /dev/null +++ b/src/main/java/org/javacs/lsp/InitializeResult.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +import com.google.gson.JsonObject; + +public class InitializeResult { + public JsonObject capabilities; +} diff --git a/src/main/java/org/javacs/lsp/InsertTextFormat.java b/src/main/java/org/javacs/lsp/InsertTextFormat.java new file mode 100644 index 0000000..54adbc2 --- /dev/null +++ b/src/main/java/org/javacs/lsp/InsertTextFormat.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class InsertTextFormat { + public static final int PlainText = 1, Snippet = 2; +} diff --git a/src/main/java/org/javacs/lsp/LSP.java b/src/main/java/org/javacs/lsp/LSP.java new file mode 100644 index 0000000..06da2d7 --- /dev/null +++ b/src/main/java/org/javacs/lsp/LSP.java @@ -0,0 +1,361 @@ +package org.javacs.lsp; + +import com.google.common.base.Charsets; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import java.io.*; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LSP { + private static final Gson gson = new Gson(); + + private static String readHeader(InputStream client) { + var line = new StringBuilder(); + for (var next = read(client); next != -1; next = read(client)) { + if (next == '\r') { + var last = read(client); + assert last == '\n'; + break; + } + line.append((char) next); + } + return line.toString(); + } + + private static int parseHeader(String header) { + var contentLength = "Content-Length: "; + if (header.startsWith(contentLength)) { + var tail = header.substring(contentLength.length()); + var length = Integer.parseInt(tail); + return length; + } + return -1; + } + + private static int read(InputStream client) { + try { + return client.read(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static String readLength(InputStream client, int byteLength) { + // Eat whitespace + // Have observed problems with extra \r\n sequences from VSCode + var next = read(client); + while (next != -1 && 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); + i++; + if (i == byteLength) break; + next = read(client); + } + return result.toString(); + } + + static String nextToken(InputStream client) { + var contentLength = -1; + while (true) { + var line = readHeader(client); + // If header is empty, next line is the start of the message + if (line.isEmpty()) return readLength(client, contentLength); + // If header contains length, save it + var maybeLength = parseHeader(line); + if (maybeLength != -1) contentLength = maybeLength; + } + } + + static Message parseMessage(String token) { + return gson.fromJson(token, Message.class); + } + + private static void writeClient(OutputStream client, String messageText) { + var messageBytes = messageText.getBytes(Charsets.UTF_8); + var headerText = String.format("Content-Length: %d\r\n\r\n", messageBytes.length); + var headerBytes = headerText.getBytes(Charsets.UTF_8); + try { + client.write(headerBytes); + client.write(messageBytes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + static void respond(OutputStream client, int requestId, Object params) { + 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) { + var jsonText = gson.toJson(params); + var messageText = String.format("{\"jsonrpc\":\"2.0\",\"method\":\"%s\",\"params\":%s}", method, jsonText); + writeClient(client, messageText); + } + + private static class RealClient implements LanguageClient { + final OutputStream send; + + RealClient(OutputStream send) { + this.send = send; + } + + @Override + public void publishDiagnostics(PublishDiagnosticsParams params) { + var json = gson.toJson(params); + notifyClient(send, "textDocument/publishDiagnostics", json); + } + + @Override + public void showMessage(ShowMessageParams params) { + var json = gson.toJson(params); + notifyClient(send, "window/showMessage", json); + } + + @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); + } + + @Override + public void customNotification(String method, JsonElement params) { + var json = gson.toJson(params); + notifyClient(send, method, json); + } + } + + 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); + + // 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)); + 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)); + } + } + + @Override + public void run() { + LOG.info("Placing incoming messages on queue..."); + + while (true) { + try { + 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())); + } catch (Exception e) { + LOG.log(Level.SEVERE, e.getMessage(), e); + } + } + } + } + Thread reader = new Thread(new MessageReader(), "reader"); + reader.setDaemon(true); + reader.start(); + + // Process messages on main thread + LOG.info("Reading messages from queue..."); + 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) { + case "initialize": + { + var params = gson.fromJson(request.params, InitializeParams.class); + var response = server.initialize(params); + respond(send, request.id, response); + break; + } + case "initialized": + { + server.initialized(); + break; + } + case "shutdown": + { + LOG.warning("Got shutdown message"); + break; + } + case "exit": + { + LOG.warning("Got exit message, exiting..."); + break processMessages; + } + case "workspace/didChangeWorkspaceFolders": + { + var params = gson.fromJson(request.params, DidChangeWorkspaceFoldersParams.class); + server.didChangeWorkspaceFolders(params); + break; + } + case "workspace/didChangeConfiguration": + { + var params = gson.fromJson(request.params, DidChangeConfigurationParams.class); + server.didChangeConfiguration(params); + break; + } + case "workspace/didChangeWatchedFiles": + { + var params = gson.fromJson(request.params, DidChangeWatchedFilesParams.class); + server.didChangeWatchedFiles(params); + break; + } + case "workspace/symbols": + { + var params = gson.fromJson(request.params, WorkspaceSymbolParams.class); + var response = server.workspaceSymbols(params); + respond(send, request.id, response); + break; + } + case "textDocument/didOpen": + { + var params = gson.fromJson(request.params, DidOpenTextDocumentParams.class); + server.didOpenTextDocument(params); + break; + } + case "textDocument/didChange": + { + var params = gson.fromJson(request.params, DidChangeTextDocumentParams.class); + server.didChangeTextDocument(params); + break; + } + case "textDocument/willSave": + { + var params = gson.fromJson(request.params, WillSaveTextDocumentParams.class); + server.willSaveTextDocument(params); + break; + } + case "textDocument/willSaveWaitUntil": + { + var params = gson.fromJson(request.params, WillSaveTextDocumentParams.class); + var response = server.willSaveWaitUntilTextDocument(params); + respond(send, request.id, response); + break; + } + case "textDocument/didSave": + { + var params = gson.fromJson(request.params, DidSaveTextDocumentParams.class); + server.didSaveTextDocument(params); + break; + } + case "textDocument/didClose": + { + var params = gson.fromJson(request.params, DidCloseTextDocumentParams.class); + server.didCloseTextDocument(params); + break; + } + case "textDocument/completion": + { + var params = gson.fromJson(request.params, CompletionParams.class); + var response = server.completion(params); + respond(send, request.id, response); + break; + } + case "completionItem/resolve": + { + var params = gson.fromJson(request.params, CompletionItem.class); + var response = server.resolveCompletionItem(params); + respond(send, request.id, response); + break; + } + case "textDocument/hover": + { + var params = gson.fromJson(request.params, TextDocumentPositionParams.class); + var response = server.hover(params); + respond(send, request.id, response); + break; + } + case "textDocument/signatureHelp": + { + var params = gson.fromJson(request.params, TextDocumentPositionParams.class); + var response = server.signatureHelp(params); + respond(send, request.id, response); + break; + } + case "textDocument/definition": + { + var params = gson.fromJson(request.params, TextDocumentPositionParams.class); + var response = server.gotoDefinition(params); + respond(send, request.id, response); + break; + } + case "textDocument/references": + { + var params = gson.fromJson(request.params, ReferenceParams.class); + var response = server.findReferences(params); + respond(send, request.id, response); + break; + } + case "textDocument/documentSymbol": + { + var params = gson.fromJson(request.params, DocumentSymbolParams.class); + var response = server.documentSymbol(params); + respond(send, request.id, response); + break; + } + case "textDocument/codeAction": + { + var params = gson.fromJson(request.params, CodeActionParams.class); + var response = server.codeAction(params); + respond(send, request.id, response); + break; + } + case "textDocument/codeLens": + { + var params = gson.fromJson(request.params, CodeLensParams.class); + var response = server.codeLens(params); + respond(send, request.id, response); + break; + } + case "codeLens/resolve": + { + var params = gson.fromJson(request.params, CodeLens.class); + var response = server.resolveCodeLens(params); + respond(send, request.id, response); + break; + } + case "textDocument/rename": + { + var params = gson.fromJson(request.params, RenameParams.class); + var response = server.rename(params); + respond(send, request.id, response); + break; + } + default: + LOG.warning(String.format("Don't know what to do with method `%s`", request.method)); + } + } catch (Exception e) { + LOG.log(Level.SEVERE, e.getMessage(), e); + } + } + } + + private static final Logger LOG = Logger.getLogger("main"); +} diff --git a/src/main/java/org/javacs/lsp/LanguageClient.java b/src/main/java/org/javacs/lsp/LanguageClient.java new file mode 100644 index 0000000..c603574 --- /dev/null +++ b/src/main/java/org/javacs/lsp/LanguageClient.java @@ -0,0 +1,13 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; + +public interface LanguageClient { + public void publishDiagnostics(PublishDiagnosticsParams params); + + public void showMessage(ShowMessageParams params); + + public void registerCapability(String method, JsonElement options); + + public void customNotification(String method, JsonElement params); +} diff --git a/src/main/java/org/javacs/lsp/LanguageServer.java b/src/main/java/org/javacs/lsp/LanguageServer.java new file mode 100644 index 0000000..e969c2e --- /dev/null +++ b/src/main/java/org/javacs/lsp/LanguageServer.java @@ -0,0 +1,102 @@ +package org.javacs.lsp; + +import java.util.List; +import java.util.Optional; + +public class LanguageServer { + public InitializeResult initialize(InitializeParams params) { + throw new RuntimeException("Unimplemented"); + } + + public void initialized() { + throw new RuntimeException("Unimplemented"); + } + + public void shutdown() { + throw new RuntimeException("Unimplemented"); + } + + public void didChangeConfiguration(DidChangeConfigurationParams params) { + throw new RuntimeException("Unimplemented"); + } + + public void didOpenTextDocument(DidOpenTextDocumentParams params) { + throw new RuntimeException("Unimplemented"); + } + + public void didChangeTextDocument(DidChangeTextDocumentParams params) { + throw new RuntimeException("Unimplemented"); + } + + public void willSaveTextDocument(WillSaveTextDocumentParams params) { + throw new RuntimeException("Unimplemented"); + } + + public List<TextEdit> willSaveWaitUntilTextDocument(WillSaveTextDocumentParams params) { + throw new RuntimeException("Unimplemented"); + } + + public void didSaveTextDocument(DidSaveTextDocumentParams params) { + throw new RuntimeException("Unimplemented"); + } + + public void didCloseTextDocument(DidCloseTextDocumentParams params) { + throw new RuntimeException("Unimplemented"); + } + + public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { + throw new RuntimeException("Unimplemented"); + } + + public Optional<CompletionList> completion(TextDocumentPositionParams params) { + throw new RuntimeException("Unimplemented"); + } + + public CompletionItem resolveCompletionItem(CompletionItem params) { + throw new RuntimeException("Unimplemented"); + } + + public Optional<Hover> hover(TextDocumentPositionParams params) { + throw new RuntimeException("Unimplemented"); + } + + public Optional<SignatureHelp> signatureHelp(TextDocumentPositionParams params) { + throw new RuntimeException("Unimplemented"); + } + + public List<Location> gotoDefinition(TextDocumentPositionParams params) { + throw new RuntimeException("Unimplemented"); + } + + public List<Location> findReferences(ReferenceParams params) { + throw new RuntimeException("Unimplemented"); + } + + public List<SymbolInformation> documentSymbol(DocumentSymbolParams params) { + throw new RuntimeException("Unimplemented"); + } + + public List<SymbolInformation> workspaceSymbols(WorkspaceSymbolParams params) { + throw new RuntimeException("Unimplemented"); + } + + public List<Command> codeAction(CodeActionParams params) { + throw new RuntimeException("Unimplemented"); + } + + public List<CodeLens> codeLens(CodeLensParams params) { + throw new RuntimeException("Unimplemented"); + } + + public CodeLens resolveCodeLens(CodeLens params) { + throw new RuntimeException("Unimplemented"); + } + + public WorkspaceEdit rename(RenameParams params) { + throw new RuntimeException("Unimplemented"); + } + + public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams 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 new file mode 100644 index 0000000..b35bf82 --- /dev/null +++ b/src/main/java/org/javacs/lsp/Location.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.net.URI; + +public class Location { + URI uri; + Range range; +} diff --git a/src/main/java/org/javacs/lsp/MarkedString.java b/src/main/java/org/javacs/lsp/MarkedString.java new file mode 100644 index 0000000..da0f33f --- /dev/null +++ b/src/main/java/org/javacs/lsp/MarkedString.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class MarkedString { + public String language, value; +} diff --git a/src/main/java/org/javacs/lsp/MarkupContent.java b/src/main/java/org/javacs/lsp/MarkupContent.java new file mode 100644 index 0000000..3e2d58f --- /dev/null +++ b/src/main/java/org/javacs/lsp/MarkupContent.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class MarkupContent { + public MarkupKind kind; + public String value; +} diff --git a/src/main/java/org/javacs/lsp/MarkupKind.java b/src/main/java/org/javacs/lsp/MarkupKind.java new file mode 100644 index 0000000..7706dbf --- /dev/null +++ b/src/main/java/org/javacs/lsp/MarkupKind.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class MarkupKind { + public static final String PlainText = "plaintext", Markdown = "markdown"; +} diff --git a/src/main/java/org/javacs/lsp/Message.java b/src/main/java/org/javacs/lsp/Message.java new file mode 100644 index 0000000..3998c0b --- /dev/null +++ b/src/main/java/org/javacs/lsp/Message.java @@ -0,0 +1,10 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; + +public class Message { + public String jsonrpc; + public Integer id; + public String method; + public JsonElement params; +} diff --git a/src/main/java/org/javacs/lsp/MessageType.java b/src/main/java/org/javacs/lsp/MessageType.java new file mode 100644 index 0000000..35aeccb --- /dev/null +++ b/src/main/java/org/javacs/lsp/MessageType.java @@ -0,0 +1,12 @@ +package org.javacs.lsp; + +public class MessageType { + /** An error message. */ + public static final int Error = 1; + /** A warning message. */ + public static final int Warning = 2; + /** An information message. */ + public static final int Info = 3; + /** A log message. */ + public static final int Log = 4; +} diff --git a/src/main/java/org/javacs/lsp/NotificationMessage.java b/src/main/java/org/javacs/lsp/NotificationMessage.java new file mode 100644 index 0000000..6e46a20 --- /dev/null +++ b/src/main/java/org/javacs/lsp/NotificationMessage.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; + +public class NotificationMessage { + public String method; + public JsonElement params; +} diff --git a/src/main/java/org/javacs/lsp/ParameterInformation.java b/src/main/java/org/javacs/lsp/ParameterInformation.java new file mode 100644 index 0000000..0692bbb --- /dev/null +++ b/src/main/java/org/javacs/lsp/ParameterInformation.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class ParameterInformation { + public String label; + public MarkupContent documentation; +} diff --git a/src/main/java/org/javacs/lsp/Position.java b/src/main/java/org/javacs/lsp/Position.java new file mode 100644 index 0000000..ae6b466 --- /dev/null +++ b/src/main/java/org/javacs/lsp/Position.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class Position { + // 0-based + public int line, character; +} diff --git a/src/main/java/org/javacs/lsp/PublishDiagnosticsParams.java b/src/main/java/org/javacs/lsp/PublishDiagnosticsParams.java new file mode 100644 index 0000000..8e19d05 --- /dev/null +++ b/src/main/java/org/javacs/lsp/PublishDiagnosticsParams.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +import java.net.URI; +import java.util.List; + +public class PublishDiagnosticsParams { + public URI uri; + public List<Diagnostic> diagnostics; +} diff --git a/src/main/java/org/javacs/lsp/README.md b/src/main/java/org/javacs/lsp/README.md new file mode 100644 index 0000000..d90ceb4 --- /dev/null +++ b/src/main/java/org/javacs/lsp/README.md @@ -0,0 +1,8 @@ +# LSP module +`org.javacs.lsp` is a minimalist implementation of the Language Server Protocol. +It doesn't necessarily implement all features of the LSP, because it's only intended to serve the purposes of the Java Language Server. + +## Why not use https://github.com/eclipse/lsp4j? +One of the design goals of the JLS is to be deployed as a zero-dependency native application, +so that it works correctly regardless of what version of Java you have on your system. +`lsp4j` isn't compatible with the Java Module System, which we need in order to use jlink.
\ No newline at end of file diff --git a/src/main/java/org/javacs/lsp/Range.java b/src/main/java/org/javacs/lsp/Range.java new file mode 100644 index 0000000..0d0e3c0 --- /dev/null +++ b/src/main/java/org/javacs/lsp/Range.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class Range { + public Position start, end; +} diff --git a/src/main/java/org/javacs/lsp/ReferenceContext.java b/src/main/java/org/javacs/lsp/ReferenceContext.java new file mode 100644 index 0000000..7451854 --- /dev/null +++ b/src/main/java/org/javacs/lsp/ReferenceContext.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class ReferenceContext { + public boolean includeDeclaration; +} diff --git a/src/main/java/org/javacs/lsp/ReferenceParams.java b/src/main/java/org/javacs/lsp/ReferenceParams.java new file mode 100644 index 0000000..c58be0d --- /dev/null +++ b/src/main/java/org/javacs/lsp/ReferenceParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class ReferenceParams extends TextDocumentPositionParams { + public ReferenceContext context; +} diff --git a/src/main/java/org/javacs/lsp/RegistrationParams.java b/src/main/java/org/javacs/lsp/RegistrationParams.java new file mode 100644 index 0000000..9030aa0 --- /dev/null +++ b/src/main/java/org/javacs/lsp/RegistrationParams.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; + +public class RegistrationParams { + public String id, method; + public JsonElement registerOptions; +} diff --git a/src/main/java/org/javacs/lsp/RenameParams.java b/src/main/java/org/javacs/lsp/RenameParams.java new file mode 100644 index 0000000..6e4a897 --- /dev/null +++ b/src/main/java/org/javacs/lsp/RenameParams.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +public class RenameParams { + public TextDocumentIdentifier textDocument; + public Position position; + public String newName; +} diff --git a/src/main/java/org/javacs/lsp/RequestMessage.java b/src/main/java/org/javacs/lsp/RequestMessage.java new file mode 100644 index 0000000..d214108 --- /dev/null +++ b/src/main/java/org/javacs/lsp/RequestMessage.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; + +public class RequestMessage { + public String id, method; + public JsonElement params; +} diff --git a/src/main/java/org/javacs/lsp/ResponseError.java b/src/main/java/org/javacs/lsp/ResponseError.java new file mode 100644 index 0000000..d6a19c8 --- /dev/null +++ b/src/main/java/org/javacs/lsp/ResponseError.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; + +public class ResponseError { + public int code; + public String message; + public JsonElement data; +} diff --git a/src/main/java/org/javacs/lsp/ResponseMessage.java b/src/main/java/org/javacs/lsp/ResponseMessage.java new file mode 100644 index 0000000..d982de3 --- /dev/null +++ b/src/main/java/org/javacs/lsp/ResponseMessage.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +import com.google.gson.JsonElement; + +public class ResponseMessage { + public String id; + public JsonElement result; + public ResponseError error; +} diff --git a/src/main/java/org/javacs/lsp/ShowMessageParams.java b/src/main/java/org/javacs/lsp/ShowMessageParams.java new file mode 100644 index 0000000..9ac0938 --- /dev/null +++ b/src/main/java/org/javacs/lsp/ShowMessageParams.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class ShowMessageParams { + public int type; + public String message; +} diff --git a/src/main/java/org/javacs/lsp/SignatureHelp.java b/src/main/java/org/javacs/lsp/SignatureHelp.java new file mode 100644 index 0000000..eb25037 --- /dev/null +++ b/src/main/java/org/javacs/lsp/SignatureHelp.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +import java.util.List; + +public class SignatureHelp { + public List<SignatureInformation> signatures; + public Integer activeSignature; + public Integer activeParameter; +} diff --git a/src/main/java/org/javacs/lsp/SignatureInformation.java b/src/main/java/org/javacs/lsp/SignatureInformation.java new file mode 100644 index 0000000..b5a7f61 --- /dev/null +++ b/src/main/java/org/javacs/lsp/SignatureInformation.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +import java.util.List; + +public class SignatureInformation { + public String label; + public MarkupContent documentation; + public List<ParameterInformation> parameters; +} diff --git a/src/main/java/org/javacs/lsp/SymbolInformation.java b/src/main/java/org/javacs/lsp/SymbolInformation.java new file mode 100644 index 0000000..fb1e0ad --- /dev/null +++ b/src/main/java/org/javacs/lsp/SymbolInformation.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +public class SymbolInformation { + public String name; + public int kind; + public boolean deprecated; + public Location location; + public String containerName; +} diff --git a/src/main/java/org/javacs/lsp/SymbolKind.java b/src/main/java/org/javacs/lsp/SymbolKind.java new file mode 100644 index 0000000..9a9880d --- /dev/null +++ b/src/main/java/org/javacs/lsp/SymbolKind.java @@ -0,0 +1,30 @@ +package org.javacs.lsp; + +public class SymbolKind { + public static final int File = 1, + Module = 2, + Namespace = 3, + Package = 4, + Class = 5, + Method = 6, + Property = 7, + Field = 8, + Constructor = 9, + Enum = 10, + Interface = 11, + Function = 12, + Variable = 13, + Constant = 14, + String = 15, + Number = 16, + Boolean = 17, + Array = 18, + Object = 19, + Key = 20, + Null = 21, + EnumMember = 22, + Struct = 23, + Event = 24, + Operator = 25, + TypeParameter = 26; +} diff --git a/src/main/java/org/javacs/lsp/TextDocumentContentChangeEvent.java b/src/main/java/org/javacs/lsp/TextDocumentContentChangeEvent.java new file mode 100644 index 0000000..366b187 --- /dev/null +++ b/src/main/java/org/javacs/lsp/TextDocumentContentChangeEvent.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +public class TextDocumentContentChangeEvent { + public Range range; + public Integer rangeLength; + public String text; +} diff --git a/src/main/java/org/javacs/lsp/TextDocumentEdit.java b/src/main/java/org/javacs/lsp/TextDocumentEdit.java new file mode 100644 index 0000000..0078de0 --- /dev/null +++ b/src/main/java/org/javacs/lsp/TextDocumentEdit.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.util.List; + +public class TextDocumentEdit { + public VersionedTextDocumentIdentifier textDocument; + public List<TextEdit> edits; +} diff --git a/src/main/java/org/javacs/lsp/TextDocumentIdentifier.java b/src/main/java/org/javacs/lsp/TextDocumentIdentifier.java new file mode 100644 index 0000000..dce19b8 --- /dev/null +++ b/src/main/java/org/javacs/lsp/TextDocumentIdentifier.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +import java.net.URI; + +public class TextDocumentIdentifier { + public URI uri; +} diff --git a/src/main/java/org/javacs/lsp/TextDocumentItem.java b/src/main/java/org/javacs/lsp/TextDocumentItem.java new file mode 100644 index 0000000..2a68cf6 --- /dev/null +++ b/src/main/java/org/javacs/lsp/TextDocumentItem.java @@ -0,0 +1,10 @@ +package org.javacs.lsp; + +import java.net.URI; + +public class TextDocumentItem { + public URI uri; + public String languageId; + public int version; + public String text; +} diff --git a/src/main/java/org/javacs/lsp/TextDocumentPositionParams.java b/src/main/java/org/javacs/lsp/TextDocumentPositionParams.java new file mode 100644 index 0000000..1d719cb --- /dev/null +++ b/src/main/java/org/javacs/lsp/TextDocumentPositionParams.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class TextDocumentPositionParams { + public TextDocumentIdentifier textDocument; + public Position position; +} diff --git a/src/main/java/org/javacs/lsp/TextDocumentSaveReason.java b/src/main/java/org/javacs/lsp/TextDocumentSaveReason.java new file mode 100644 index 0000000..1f39e56 --- /dev/null +++ b/src/main/java/org/javacs/lsp/TextDocumentSaveReason.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class TextDocumentSaveReason { + public static final int Manual = 1, AfterDelay = 2, FocusOut = 3; +} diff --git a/src/main/java/org/javacs/lsp/TextEdit.java b/src/main/java/org/javacs/lsp/TextEdit.java new file mode 100644 index 0000000..03fe4e5 --- /dev/null +++ b/src/main/java/org/javacs/lsp/TextEdit.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class TextEdit { + public Range range; + public String newText; +} diff --git a/src/main/java/org/javacs/lsp/VersionedTextDocumentIdentifier.java b/src/main/java/org/javacs/lsp/VersionedTextDocumentIdentifier.java new file mode 100644 index 0000000..c374e3b --- /dev/null +++ b/src/main/java/org/javacs/lsp/VersionedTextDocumentIdentifier.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.net.URI; + +public class VersionedTextDocumentIdentifier { + public URI uri; + public int version; +} diff --git a/src/main/java/org/javacs/lsp/WillSaveTextDocumentParams.java b/src/main/java/org/javacs/lsp/WillSaveTextDocumentParams.java new file mode 100644 index 0000000..df6b181 --- /dev/null +++ b/src/main/java/org/javacs/lsp/WillSaveTextDocumentParams.java @@ -0,0 +1,6 @@ +package org.javacs.lsp; + +public class WillSaveTextDocumentParams { + public TextDocumentIdentifier textDocument; + public int reason; +} diff --git a/src/main/java/org/javacs/lsp/WorkspaceEdit.java b/src/main/java/org/javacs/lsp/WorkspaceEdit.java new file mode 100644 index 0000000..e2cffa1 --- /dev/null +++ b/src/main/java/org/javacs/lsp/WorkspaceEdit.java @@ -0,0 +1,9 @@ +package org.javacs.lsp; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +public class WorkspaceEdit { + public Map<URI, List<TextEdit>> changes; +} diff --git a/src/main/java/org/javacs/lsp/WorkspaceFolder.java b/src/main/java/org/javacs/lsp/WorkspaceFolder.java new file mode 100644 index 0000000..0d5c6ec --- /dev/null +++ b/src/main/java/org/javacs/lsp/WorkspaceFolder.java @@ -0,0 +1,8 @@ +package org.javacs.lsp; + +import java.net.URI; + +public class WorkspaceFolder { + public URI uri; + public String name; +} diff --git a/src/main/java/org/javacs/lsp/WorkspaceFoldersChangeEvent.java b/src/main/java/org/javacs/lsp/WorkspaceFoldersChangeEvent.java new file mode 100644 index 0000000..0cac0c9 --- /dev/null +++ b/src/main/java/org/javacs/lsp/WorkspaceFoldersChangeEvent.java @@ -0,0 +1,7 @@ +package org.javacs.lsp; + +import java.util.List; + +public class WorkspaceFoldersChangeEvent { + public List<WorkspaceFolder> added, removed; +} diff --git a/src/main/java/org/javacs/lsp/WorkspaceSymbolParams.java b/src/main/java/org/javacs/lsp/WorkspaceSymbolParams.java new file mode 100644 index 0000000..d3859f9 --- /dev/null +++ b/src/main/java/org/javacs/lsp/WorkspaceSymbolParams.java @@ -0,0 +1,5 @@ +package org.javacs.lsp; + +public class WorkspaceSymbolParams { + public String query; +} diff --git a/src/test/java/org/javacs/lsp/LanguageServerTest.java b/src/test/java/org/javacs/lsp/LanguageServerTest.java new file mode 100644 index 0000000..75a1faf --- /dev/null +++ b/src/test/java/org/javacs/lsp/LanguageServerTest.java @@ -0,0 +1,74 @@ +package org.javacs.lsp; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.javacs.Main; +import org.junit.Before; +import org.junit.Test; + +public class LanguageServerTest { + PipedInputStream clientToServer = new PipedInputStream(10 * 1024 * 1024), + serverToClient = new PipedInputStream(10 * 1024 * 1024); + PipedOutputStream writeClientToServer, writeServerToClient; + LanguageServer mockServer; + Thread main; + CompletableFuture<Void> receivedInitialize = new CompletableFuture<>(); + + class TestLanguageServer extends LanguageServer { + @Override + public InitializeResult initialize(InitializeParams params) { + receivedInitialize.complete(null); + return new InitializeResult(); + } + } + + static { + Main.setRootFormat(); + } + + @Before + public void connectServerAndInitialize() throws IOException { + writeClientToServer = new PipedOutputStream(clientToServer); + writeServerToClient = new PipedOutputStream(serverToClient); + main = new Thread(this::runServer, "runServer"); + main.start(); + } + + private void runServer() { + LSP.connect(this::serverFactory, clientToServer, writeServerToClient); + } + + private LanguageServer serverFactory(LanguageClient client) { + mockServer = new TestLanguageServer(); + return mockServer; + } + + private void sendToServer(String message) throws IOException { + var header = String.format("Content-Length: %d\r\n\r\n", message.getBytes().length); + writeClientToServer.write(header.getBytes()); + writeClientToServer.write(message.getBytes()); + } + + String initializeMessage = "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}"; + String exitMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}"; + + @Test + public void exitMessageKillsServer() + throws IOException, InterruptedException, ExecutionException, TimeoutException { + // Send initialize message and wait for ack + sendToServer(initializeMessage); + receivedInitialize.get(1, TimeUnit.SECONDS); + // Send exit message and wait for exit + sendToServer(exitMessage); + main.join(1000); + 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 new file mode 100644 index 0000000..3f65774 --- /dev/null +++ b/src/test/java/org/javacs/lsp/LspTest.java @@ -0,0 +1,65 @@ +package org.javacs.lsp; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +import com.google.common.base.Charsets; +import com.google.gson.JsonObject; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import org.junit.Before; +import org.junit.Test; + +public class LspTest { + PipedInputStream buffer = new PipedInputStream(10 * 1024 * 1024); // 10 MB buffer + PipedOutputStream writer = new PipedOutputStream(); + + @Before + public void connectBuffer() throws IOException { + writer.connect(buffer); + } + + String bufferToString() { + try { + var available = buffer.available(); + var bytes = new byte[available]; + var read = buffer.read(bytes); + assert read == available; + return new String(bytes, Charsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + public void writeResponse() { + LSP.respond(writer, 1, 2); + var expected = "Content-Length: 35\r\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":2}"; + assertThat(bufferToString(), equalTo(expected)); + } + + @Test + public void writeMultibyteCharacters() { + LSP.respond(writer, 1, "🔥"); + var expected = "Content-Length: 40\r\n\r\n{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"🔥\"}"; + 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); + writer.write(header.getBytes()); + writer.write(message.getBytes()); + + var token = LSP.nextToken(buffer); + assertThat(token, equalTo(message)); + + var parse = LSP.parseMessage(token); + assertThat(parse.jsonrpc, equalTo("2.0")); + assertThat(parse.id, equalTo(1)); + assertThat(parse.method, equalTo("initialize")); + assertThat(parse.params, equalTo(new JsonObject())); + } +} |