From 3bd33cdf7ee125916707b7be97a0df98dc3e0dc3 Mon Sep 17 00:00:00 2001 From: John Lee Date: Mon, 1 Mar 2021 22:49:27 +0000 Subject: [PATCH 1/3] Add --stdio switch motivated by LSP clients such as emacs lsp-mode.el that don't support communication over node IPC --- server/src/server.ts | 73 ++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index f6673703b..ac614b7f0 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -2,6 +2,7 @@ import process from "process"; import * as p from "vscode-languageserver-protocol"; import * as m from "vscode-jsonrpc/lib/messages"; import * as v from "vscode-languageserver"; +import * as rpc from "vscode-jsonrpc"; import * as path from "path"; import fs from "fs"; // TODO: check DidChangeWatchedFilesNotification. @@ -39,6 +40,8 @@ let projectsFiles: Map< > = new Map(); // ^ caching AND states AND distributed system. Why does LSP has to be stupid like this +let messageSender: any = {send: (msg: m.Message) => process.send!(msg)}; + let sendUpdatedDiagnostics = () => { projectsFiles.forEach(({ filesWithDiagnostics }, projectRootPath) => { let content = fs.readFileSync( @@ -60,7 +63,7 @@ let sendUpdatedDiagnostics = () => { method: "textDocument/publishDiagnostics", params: params, }; - process.send!(notification); + messageSender.send(notification); filesWithDiagnostics.add(file); }); @@ -78,7 +81,7 @@ let sendUpdatedDiagnostics = () => { method: "textDocument/publishDiagnostics", params: params, }; - process.send!(notification); + messageSender.send(notification); filesWithDiagnostics.delete(file); } }); @@ -98,7 +101,7 @@ let deleteProjectDiagnostics = (projectRootPath: string) => { method: "textDocument/publishDiagnostics", params: params, }; - process.send!(notification); + messageSender.send(notification); }); projectsFiles.delete(projectRootPath); @@ -167,7 +170,7 @@ let openedFile = (fileUri: string, fileContent: string) => { method: "window/showMessageRequest", params: params, }; - process.send!(request); + messageSender.send(request); // the client might send us back the "start build" action, which we'll // handle in the isResponseMessage check in the message handling way // below @@ -216,7 +219,31 @@ let getOpenedFileContent = (fileUri: string) => { return content; }; -process.on("message", (msg: m.Message) => { +let onMessage = (func: any) => { + process.on("message", (msg: m.Message) => { + func(messageSender.send, msg); + }) +}; + +let argv = process.argv.slice(2); +for (let i = 0; i < argv.length; i++) { + let arg = argv[i]; + if (arg === "--stdio") { + let writer = new rpc.StreamMessageWriter(process.stdout); + let reader = new rpc.StreamMessageReader(process.stdin); + messageSender.send = (msg: m.Message) => writer.write(msg); + onMessage = ((func: any) => { + let callback = (message: m.Message) => { + func(messageSender.send, message); + } + reader.listen(callback); + }); + + break; + }; +} + +onMessage((send: any, msg: m.Message) => { if (m.isNotificationMessage(msg)) { // notification message, aka the client ends it and doesn't want a reply if (!initialized && msg.method !== "exit") { @@ -266,7 +293,7 @@ process.on("message", (msg: m.Message) => { message: "Server not initialized.", }, }; - process.send!(response); + send(response); } else if (msg.method === "initialize") { // send the list of features we support let result: p.InitializeResult = { @@ -290,7 +317,7 @@ process.on("message", (msg: m.Message) => { result: result, }; initialized = true; - process.send!(response); + send(response); } else if (msg.method === "initialized") { // sent from client after initialize. Nothing to do for now let response: m.ResponseMessage = { @@ -298,7 +325,7 @@ process.on("message", (msg: m.Message) => { id: msg.id, result: null, }; - process.send!(response); + send(response); } else if (msg.method === "shutdown") { // https://microsoft.github.io/language-server-protocol/specification#shutdown if (shutdownRequestAlreadyReceived) { @@ -310,7 +337,7 @@ process.on("message", (msg: m.Message) => { message: `Language server already received the shutdown request`, }, }; - process.send!(response); + send(response); } else { shutdownRequestAlreadyReceived = true; // TODO: recheck logic around init/shutdown... @@ -322,7 +349,7 @@ process.on("message", (msg: m.Message) => { id: msg.id, result: null, }; - process.send!(response); + send(response); } } else if (msg.method === p.HoverRequest.method) { let emptyHoverResponse: m.ResponseMessage = { @@ -338,9 +365,9 @@ process.on("message", (msg: m.Message) => { ...emptyHoverResponse, result: { contents: result.hover }, }; - process.send!(hoverResponse); + send(hoverResponse); } else { - process.send!(emptyHoverResponse); + send(emptyHoverResponse); } } else if (msg.method === p.DefinitionRequest.method) { // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition @@ -361,9 +388,9 @@ process.on("message", (msg: m.Message) => { range: result.definition.range, }, }; - process.send!(definitionResponse); + send(definitionResponse); } else { - process.send!(emptyDefinitionResponse); + send(emptyDefinitionResponse); } } else if (msg.method === p.CompletionRequest.method) { let emptyCompletionResponse: m.ResponseMessage = { @@ -374,13 +401,13 @@ process.on("message", (msg: m.Message) => { let code = getOpenedFileContent(msg.params.textDocument.uri); let result = runCompletionCommand(msg, code); if (result === null) { - process.send!(emptyCompletionResponse); + send(emptyCompletionResponse); } else { let definitionResponse: m.ResponseMessage = { ...emptyCompletionResponse, result: result, }; - process.send!(definitionResponse); + send(definitionResponse); } } else if (msg.method === p.DocumentFormattingRequest.method) { // technically, a formatting failure should reply with the error. Sadly @@ -409,8 +436,8 @@ process.on("message", (msg: m.Message) => { method: "window/showMessage", params: params, }; - process.send!(fakeSuccessResponse); - process.send!(response); + send(fakeSuccessResponse); + send(response); } else { // See comment on findBscExeDirOfFile for why we need // to recursively search for bsc.exe upward @@ -425,8 +452,8 @@ process.on("message", (msg: m.Message) => { method: "window/showMessage", params: params, }; - process.send!(fakeSuccessResponse); - process.send!(response); + send(fakeSuccessResponse); + send(response); } else { let resolvedBscExePath = path.join(bscExeDir, c.bscExePartialPath); // code will always be defined here, even though technically it can be undefined @@ -454,13 +481,13 @@ process.on("message", (msg: m.Message) => { id: msg.id, result: result, }; - process.send!(response); + send(response); } else { // let the diagnostics logic display the updated syntax errors, // from the build. // Again, not sending the actual errors. See fakeSuccessResponse // above for explanation - process.send!(fakeSuccessResponse); + send(fakeSuccessResponse); } } } @@ -473,7 +500,7 @@ process.on("message", (msg: m.Message) => { message: "Unrecognized editor request.", }, }; - process.send!(response); + send(response); } } else if (m.isResponseMessage(msg)) { // response message. Currently the client should have only sent a response From 223c8885457a0e93c22bc10c3dcde54286e8ec23 Mon Sep 17 00:00:00 2001 From: John Lee Date: Sun, 21 Mar 2021 15:49:51 +0000 Subject: [PATCH 2/3] Clean up --stdio switch --- server/src/server.ts | 69 +++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index ac614b7f0..ac790a5c6 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -40,7 +40,40 @@ let projectsFiles: Map< > = new Map(); // ^ caching AND states AND distributed system. Why does LSP has to be stupid like this -let messageSender: any = {send: (msg: m.Message) => process.send!(msg)}; +type messageHandler = (send: (msg: m.Message) => void, message: m.Message) => void; + +let makeStdioChannel = () => { + let writer = new rpc.StreamMessageWriter(process.stdout); + let reader = new rpc.StreamMessageReader(process.stdin); + let send = (msg: m.Message) => writer.write(msg); + return { + onMessage: ((func: messageHandler) => { + let callback = (message: m.Message) => { + func(send, message); + } + reader.listen(callback); + }), + send, + }; +} + +let makeNodeIpcChannel = () => { + let send = (msg: m.Message) => process.send!(msg); + return { + onMessage: (func: messageHandler) => { + process.on("message", (msg: m.Message) => { + func(send, msg); + }) + }, + send, + }; +} + +let channel = ( + process.argv.includes("--stdio") + ? makeStdioChannel() + : makeNodeIpcChannel() +); let sendUpdatedDiagnostics = () => { projectsFiles.forEach(({ filesWithDiagnostics }, projectRootPath) => { @@ -63,7 +96,7 @@ let sendUpdatedDiagnostics = () => { method: "textDocument/publishDiagnostics", params: params, }; - messageSender.send(notification); + channel.send(notification); filesWithDiagnostics.add(file); }); @@ -81,7 +114,7 @@ let sendUpdatedDiagnostics = () => { method: "textDocument/publishDiagnostics", params: params, }; - messageSender.send(notification); + channel.send(notification); filesWithDiagnostics.delete(file); } }); @@ -101,7 +134,7 @@ let deleteProjectDiagnostics = (projectRootPath: string) => { method: "textDocument/publishDiagnostics", params: params, }; - messageSender.send(notification); + channel.send(notification); }); projectsFiles.delete(projectRootPath); @@ -170,7 +203,7 @@ let openedFile = (fileUri: string, fileContent: string) => { method: "window/showMessageRequest", params: params, }; - messageSender.send(request); + channel.send(request); // the client might send us back the "start build" action, which we'll // handle in the isResponseMessage check in the message handling way // below @@ -219,31 +252,7 @@ let getOpenedFileContent = (fileUri: string) => { return content; }; -let onMessage = (func: any) => { - process.on("message", (msg: m.Message) => { - func(messageSender.send, msg); - }) -}; - -let argv = process.argv.slice(2); -for (let i = 0; i < argv.length; i++) { - let arg = argv[i]; - if (arg === "--stdio") { - let writer = new rpc.StreamMessageWriter(process.stdout); - let reader = new rpc.StreamMessageReader(process.stdin); - messageSender.send = (msg: m.Message) => writer.write(msg); - onMessage = ((func: any) => { - let callback = (message: m.Message) => { - func(messageSender.send, message); - } - reader.listen(callback); - }); - - break; - }; -} - -onMessage((send: any, msg: m.Message) => { +channel.onMessage((send, msg: m.Message) => { if (m.isNotificationMessage(msg)) { // notification message, aka the client ends it and doesn't want a reply if (!initialized && msg.method !== "exit") { From 1873b1d2ab8ebf822778870c3a6dabced2470a81 Mon Sep 17 00:00:00 2001 From: Cheng Lou Date: Wed, 31 Mar 2021 18:10:20 -0700 Subject: [PATCH 3/3] Cleanup --- server/src/server.ts | 63 ++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index ac790a5c6..696676431 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -40,40 +40,8 @@ let projectsFiles: Map< > = new Map(); // ^ caching AND states AND distributed system. Why does LSP has to be stupid like this -type messageHandler = (send: (msg: m.Message) => void, message: m.Message) => void; - -let makeStdioChannel = () => { - let writer = new rpc.StreamMessageWriter(process.stdout); - let reader = new rpc.StreamMessageReader(process.stdin); - let send = (msg: m.Message) => writer.write(msg); - return { - onMessage: ((func: messageHandler) => { - let callback = (message: m.Message) => { - func(send, message); - } - reader.listen(callback); - }), - send, - }; -} - -let makeNodeIpcChannel = () => { - let send = (msg: m.Message) => process.send!(msg); - return { - onMessage: (func: messageHandler) => { - process.on("message", (msg: m.Message) => { - func(send, msg); - }) - }, - send, - }; -} - -let channel = ( - process.argv.includes("--stdio") - ? makeStdioChannel() - : makeNodeIpcChannel() -); +// will be properly defined later depending on the mode (stdio/node-rpc) +let send: (msg: m.Message) => void = (_) => { }; let sendUpdatedDiagnostics = () => { projectsFiles.forEach(({ filesWithDiagnostics }, projectRootPath) => { @@ -96,7 +64,7 @@ let sendUpdatedDiagnostics = () => { method: "textDocument/publishDiagnostics", params: params, }; - channel.send(notification); + send(notification); filesWithDiagnostics.add(file); }); @@ -114,7 +82,7 @@ let sendUpdatedDiagnostics = () => { method: "textDocument/publishDiagnostics", params: params, }; - channel.send(notification); + send(notification); filesWithDiagnostics.delete(file); } }); @@ -134,7 +102,7 @@ let deleteProjectDiagnostics = (projectRootPath: string) => { method: "textDocument/publishDiagnostics", params: params, }; - channel.send(notification); + send(notification); }); projectsFiles.delete(projectRootPath); @@ -203,7 +171,7 @@ let openedFile = (fileUri: string, fileContent: string) => { method: "window/showMessageRequest", params: params, }; - channel.send(request); + send(request); // the client might send us back the "start build" action, which we'll // handle in the isResponseMessage check in the message handling way // below @@ -252,7 +220,22 @@ let getOpenedFileContent = (fileUri: string) => { return content; }; -channel.onMessage((send, msg: m.Message) => { +// Start listening now! +// We support two modes: the regular node RPC mode for VSCode, and the --stdio +// mode for other editors The latter is _technically unsupported_. It's an +// implementation detail that might change at any time +if (process.argv.includes("--stdio")) { + let writer = new rpc.StreamMessageWriter(process.stdout); + let reader = new rpc.StreamMessageReader(process.stdin); + // proper `this` scope for writer + send = (msg: m.Message) => writer.write(msg); + reader.listen(onMessage); +} else { + // proper `this` scope for process + send = (msg: m.Message) => process.send!(msg); + process.on("message", onMessage); +} +function onMessage(msg: m.Message) { if (m.isNotificationMessage(msg)) { // notification message, aka the client ends it and doesn't want a reply if (!initialized && msg.method !== "exit") { @@ -541,4 +524,4 @@ channel.onMessage((send, msg: m.Message) => { } } } -}); +}