From 663190e9464dbc7e2fcb50c3bb225c281c8c199f Mon Sep 17 00:00:00 2001 From: nojaf Date: Sun, 14 Dec 2025 11:48:17 +0100 Subject: [PATCH 1/2] Refactor dump command --- client/src/commands.ts | 1 - client/src/commands/dump_server_state.ts | 38 ------- client/src/extension.ts | 11 +- server/src/server.ts | 136 +++++++++++++++++------ 4 files changed, 108 insertions(+), 78 deletions(-) delete mode 100644 client/src/commands/dump_server_state.ts diff --git a/client/src/commands.ts b/client/src/commands.ts index a94c424a5..6a795c467 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -9,7 +9,6 @@ export { createInterface } from "./commands/create_interface"; export { openCompiled } from "./commands/open_compiled"; export { switchImplIntf } from "./commands/switch_impl_intf"; export { dumpDebug, dumpDebugRetrigger } from "./commands/dump_debug"; -export { dumpServerState } from "./commands/dump_server_state"; export { pasteAsRescriptJson } from "./commands/paste_as_rescript_json"; export { pasteAsRescriptJsx } from "./commands/paste_as_rescript_jsx"; diff --git a/client/src/commands/dump_server_state.ts b/client/src/commands/dump_server_state.ts deleted file mode 100644 index 5b61008c8..000000000 --- a/client/src/commands/dump_server_state.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - ExtensionContext, - StatusBarItem, - Uri, - ViewColumn, - window, -} from "vscode"; -import { LanguageClient } from "vscode-languageclient/node"; -import * as fs from "fs"; -import { createFileInTempDir } from "../utils"; - -export async function dumpServerState( - client: LanguageClient, - _context?: ExtensionContext, - _statusBarItem?: StatusBarItem, -) { - try { - const result = await client.sendRequest("rescript/dumpServerState"); - const outputFile = createFileInTempDir("server_state", ".json"); - - // Pretty-print JSON with stable ordering where possible - const replacer = (_key: string, value: any) => { - if (value instanceof Map) return Object.fromEntries(value); - if (value instanceof Set) return Array.from(value); - return value; - }; - - const json = JSON.stringify(result, replacer, 2); - fs.writeFileSync(outputFile, json, { encoding: "utf-8" }); - - await window.showTextDocument(Uri.parse(outputFile), { - viewColumn: ViewColumn.Beside, - preview: false, - }); - } catch (e) { - window.showErrorMessage(`Failed to dump server state: ${String(e)}`); - } -} diff --git a/client/src/extension.ts b/client/src/extension.ts index cd4fee0f8..5ceed6f6f 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -373,8 +373,15 @@ export function activate(context: ExtensionContext) { customCommands.dumpDebug(context, debugDumpStatusBarItem); }); - commands.registerCommand("rescript-vscode.dump-server-state", () => { - customCommands.dumpServerState(client, context, debugDumpStatusBarItem); + commands.registerCommand("rescript-vscode.dump-server-state", async () => { + // Server handles everything: writing to disk and opening the file via window/showDocument + try { + await client.sendRequest("workspace/executeCommand", { + command: "rescript/dumpServerState", + }); + } catch (e) { + console.error("Failed to dump server state:", e); + } }); commands.registerCommand("rescript-vscode.showProblems", async () => { diff --git a/server/src/server.ts b/server/src/server.ts index fccd88916..01401428b 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -5,6 +5,7 @@ import * as rpc from "vscode-jsonrpc/node"; import * as path from "path"; import semver from "semver"; import fs from "fs"; +import fsAsync from "fs/promises"; import { DidChangeWatchedFilesNotification, DidOpenTextDocumentNotification, @@ -1260,6 +1261,93 @@ function openCompiledFile(msg: p.RequestMessage): p.Message { return response; } +async function dumpServerState( + msg: p.RequestMessage, +): Promise { + // Custom debug endpoint: dump current server state (config + projectsFiles) + try { + // Read the server version from package.json + let serverVersion: string | undefined; + try { + const packageJsonPath = path.join(__dirname, "..", "package.json"); + const packageJsonContent = await fsAsync.readFile(packageJsonPath, { + encoding: "utf-8", + }); + const packageJson = JSON.parse(packageJsonContent); + serverVersion = packageJson.version; + } catch (e) { + // If we can't read the version, that's okay - we'll just omit it + serverVersion = undefined; + } + + const projects = Array.from(projectsFiles.entries()).map( + ([projectRootPath, pf]) => ({ + projectRootPath, + openFiles: Array.from(pf.openFiles), + filesWithDiagnostics: Array.from(pf.filesWithDiagnostics), + filesDiagnostics: pf.filesDiagnostics, + rescriptVersion: pf.rescriptVersion, + bscBinaryLocation: pf.bscBinaryLocation, + editorAnalysisLocation: pf.editorAnalysisLocation, + namespaceName: pf.namespaceName, + hasPromptedToStartBuild: pf.hasPromptedToStartBuild, + bsbWatcherByEditor: + pf.bsbWatcherByEditor != null + ? { pid: pf.bsbWatcherByEditor.pid ?? null } + : null, + }), + ); + + const state = { + lspServerVersion: serverVersion, + config: config.extensionConfiguration, + projects, + workspaceFolders: Array.from(workspaceFolders), + runtimePathCache: utils.getRuntimePathCacheSnapshot(), + }; + + // Format JSON with pretty-printing (2-space indent) on the server side + // This ensures consistent formatting and handles any Maps/Sets that might + // have been converted to plain objects/arrays above + const formattedJson = JSON.stringify(state, null, 2); + + // Write the file to disk on the server side + const outputFile = utils.createFileInTempDir("_server_state.json"); + fs.writeFileSync(outputFile, formattedJson, { encoding: "utf-8" }); + + // Request the client to open the document + const fileUri = utils.pathToURI(outputFile); + const showDocumentRequest: p.RequestMessage = { + jsonrpc: c.jsonrpcVersion, + id: serverSentRequestIdCounter++, + method: "window/showDocument", + params: { + uri: fileUri, + external: false, + takeFocus: true, + }, + }; + send(showDocumentRequest); + + let response: p.ResponseMessage = { + jsonrpc: c.jsonrpcVersion, + id: msg.id, + result: { uri: fileUri }, + }; + return response; + } catch (e) { + let response: p.ResponseMessage = { + jsonrpc: c.jsonrpcVersion, + id: msg.id, + error: { + code: p.ErrorCodes.InternalError, + message: `Failed to dump server state: ${String(e)}`, + }, + }; + return response; + } +} + async function onMessage(msg: p.Message) { if (p.Message.isNotification(msg)) { // notification message, aka the client ends it and doesn't want a reply @@ -1458,6 +1546,9 @@ async function onMessage(msg: p.Message) { retriggerCharacters: ["=", ","], } : undefined, + executeCommandProvider: { + commands: ["rescript/dumpServerState"], + }, }, }; let response: p.ResponseMessage = { @@ -1555,47 +1646,18 @@ async function onMessage(msg: p.Message) { if (extName === c.resExt) { send(await signatureHelp(msg)); } - } else if (msg.method === "rescript/dumpServerState") { - // Custom debug endpoint: dump current server state (config + projectsFiles) - try { - const projects = Array.from(projectsFiles.entries()).map( - ([projectRootPath, pf]) => ({ - projectRootPath, - openFiles: Array.from(pf.openFiles), - filesWithDiagnostics: Array.from(pf.filesWithDiagnostics), - filesDiagnostics: pf.filesDiagnostics, - rescriptVersion: pf.rescriptVersion, - bscBinaryLocation: pf.bscBinaryLocation, - editorAnalysisLocation: pf.editorAnalysisLocation, - namespaceName: pf.namespaceName, - hasPromptedToStartBuild: pf.hasPromptedToStartBuild, - bsbWatcherByEditor: - pf.bsbWatcherByEditor != null - ? { pid: pf.bsbWatcherByEditor.pid ?? null } - : null, - }), - ); - - const result = { - config: config.extensionConfiguration, - projects, - workspaceFolders: Array.from(workspaceFolders), - runtimePathCache: utils.getRuntimePathCacheSnapshot(), - }; - - let response: p.ResponseMessage = { - jsonrpc: c.jsonrpcVersion, - id: msg.id, - result, - }; - send(response); - } catch (e) { + } else if (msg.method === p.ExecuteCommandRequest.method) { + // Standard LSP executeCommand - supports editor-agnostic command execution + const params = msg.params as p.ExecuteCommandParams; + if (params.command === "rescript/dumpServerState") { + send(await dumpServerState(msg)); + } else { let response: p.ResponseMessage = { jsonrpc: c.jsonrpcVersion, id: msg.id, error: { - code: p.ErrorCodes.InternalError, - message: `Failed to dump server state: ${String(e)}`, + code: p.ErrorCodes.InvalidRequest, + message: `Unknown command: ${params.command}`, }, }; send(response); From 17721fe37c399bf4874b9d481e47347144f5ca1c Mon Sep 17 00:00:00 2001 From: nojaf Date: Sun, 14 Dec 2025 12:14:14 +0100 Subject: [PATCH 2/2] Avoid temp file and other code review feedback --- client/src/extension.ts | 22 +++++++++++++++++++--- server/src/server.ts | 29 ++++++++--------------------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/client/src/extension.ts b/client/src/extension.ts index 5ceed6f6f..c31a8d170 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -13,6 +13,7 @@ import { WorkspaceEdit, CodeActionKind, Diagnostic, + ViewColumn, } from "vscode"; import { ThemeColor } from "vscode"; @@ -374,13 +375,28 @@ export function activate(context: ExtensionContext) { }); commands.registerCommand("rescript-vscode.dump-server-state", async () => { - // Server handles everything: writing to disk and opening the file via window/showDocument try { - await client.sendRequest("workspace/executeCommand", { + const result = (await client.sendRequest("workspace/executeCommand", { command: "rescript/dumpServerState", + })) as { content: string }; + + // Create an unsaved document with the server state content + const document = await workspace.openTextDocument({ + content: result.content, + language: "json", + }); + + // Show the document in the editor + await window.showTextDocument(document, { + viewColumn: ViewColumn.Beside, + preview: false, }); } catch (e) { - console.error("Failed to dump server state:", e); + outputChannel.appendLine(`Failed to dump server state: ${String(e)}`); + window.showErrorMessage( + "Failed to dump server state. See 'Output' tab, 'ReScript Language Server' channel for details.", + ); + outputChannel.show(); } }); diff --git a/server/src/server.ts b/server/src/server.ts index 01401428b..77eaffcda 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1273,8 +1273,11 @@ async function dumpServerState( const packageJsonContent = await fsAsync.readFile(packageJsonPath, { encoding: "utf-8", }); - const packageJson = JSON.parse(packageJsonContent); - serverVersion = packageJson.version; + const packageJson: { version?: unknown } = JSON.parse(packageJsonContent); + serverVersion = + typeof packageJson.version === "string" + ? packageJson.version + : undefined; } catch (e) { // If we can't read the version, that's okay - we'll just omit it serverVersion = undefined; @@ -1311,28 +1314,12 @@ async function dumpServerState( // have been converted to plain objects/arrays above const formattedJson = JSON.stringify(state, null, 2); - // Write the file to disk on the server side - const outputFile = utils.createFileInTempDir("_server_state.json"); - fs.writeFileSync(outputFile, formattedJson, { encoding: "utf-8" }); - - // Request the client to open the document - const fileUri = utils.pathToURI(outputFile); - const showDocumentRequest: p.RequestMessage = { - jsonrpc: c.jsonrpcVersion, - id: serverSentRequestIdCounter++, - method: "window/showDocument", - params: { - uri: fileUri, - external: false, - takeFocus: true, - }, - }; - send(showDocumentRequest); - + // Return the content so the client can create an unsaved document + // This avoids creating temporary files that would never be cleaned up let response: p.ResponseMessage = { jsonrpc: c.jsonrpcVersion, id: msg.id, - result: { uri: fileUri }, + result: { content: formattedJson }, }; return response; } catch (e) {