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..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"; @@ -373,8 +374,30 @@ 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 () => { + try { + 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) { + 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(); + } }); commands.registerCommand("rescript-vscode.showProblems", async () => { diff --git a/server/src/server.ts b/server/src/server.ts index fccd88916..77eaffcda 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,80 @@ 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: { 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; + } + + 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); + + // 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: { content: formattedJson }, + }; + 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 +1533,9 @@ async function onMessage(msg: p.Message) { retriggerCharacters: ["=", ","], } : undefined, + executeCommandProvider: { + commands: ["rescript/dumpServerState"], + }, }, }; let response: p.ResponseMessage = { @@ -1555,47 +1633,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);