diff --git a/analysis/src/Cli.ml b/analysis/src/Cli.ml index a7529b705..a96adbed5 100644 --- a/analysis/src/Cli.ml +++ b/analysis/src/Cli.ml @@ -85,28 +85,51 @@ Options: |} let main () = - match Array.to_list Sys.argv with + let args = Array.to_list Sys.argv in + let debugLevel, args = + match args with + | _ :: "debug-dump" :: logLevel :: rest -> + ( (match logLevel with + | "verbose" -> Debug.Verbose + | "regular" -> Regular + | _ -> Off), + "dummy" :: rest ) + | args -> (Off, args) + in + Debug.debugLevel := debugLevel; + let debug = debugLevel <> Debug.Off in + let printHeaderInfo path line col = + if debug then + Printf.printf "Debug level: %s\n%s:%s-%s\n\n" + (match debugLevel with + | Debug.Verbose -> "verbose" + | Regular -> "regular" + | Off -> "off") + path line col + in + match args with | [_; "completion"; path; line; col; currentFile; supportsSnippets] -> + printHeaderInfo path line col; (Cfg.supportsSnippets := match supportsSnippets with | "true" -> true | _ -> false); - Commands.completion ~debug:false ~path + Commands.completion ~debug ~path ~pos:(int_of_string line, int_of_string col) ~currentFile | [_; "definition"; path; line; col] -> Commands.definition ~path ~pos:(int_of_string line, int_of_string col) - ~debug:false + ~debug | [_; "typeDefinition"; path; line; col] -> Commands.typeDefinition ~path ~pos:(int_of_string line, int_of_string col) - ~debug:false + ~debug | [_; "documentSymbol"; path] -> DocumentSymbol.command ~path | [_; "hover"; path; line; col; currentFile; supportsMarkdownLinks] -> Commands.hover ~path ~pos:(int_of_string line, int_of_string col) - ~currentFile ~debug:false + ~currentFile ~debug ~supportsMarkdownLinks: (match supportsMarkdownLinks with | "true" -> true @@ -114,19 +137,19 @@ let main () = | [_; "signatureHelp"; path; line; col; currentFile] -> Commands.signatureHelp ~path ~pos:(int_of_string line, int_of_string col) - ~currentFile ~debug:false + ~currentFile ~debug | [_; "inlayHint"; path; line_start; line_end; maxLength] -> Commands.inlayhint ~path ~pos:(int_of_string line_start, int_of_string line_end) - ~maxLength ~debug:false - | [_; "codeLens"; path] -> Commands.codeLens ~path ~debug:false - | [_; "extractDocs"; path] -> DocExtraction.extractDocs ~path ~debug:false + ~maxLength ~debug + | [_; "codeLens"; path] -> Commands.codeLens ~path ~debug + | [_; "extractDocs"; path] -> DocExtraction.extractDocs ~path ~debug | [_; "codeAction"; path; startLine; startCol; endLine; endCol; currentFile] -> Commands.codeAction ~path ~startPos:(int_of_string startLine, int_of_string startCol) ~endPos:(int_of_string endLine, int_of_string endCol) - ~currentFile ~debug:false + ~currentFile ~debug | [_; "codemod"; path; line; col; typ; hint] -> let typ = match typ with @@ -136,7 +159,7 @@ let main () = let res = Codemod.transform ~path ~pos:(int_of_string line, int_of_string col) - ~debug:false ~typ ~hint + ~debug ~typ ~hint |> Json.escape in Printf.printf "\"%s\"" res @@ -151,11 +174,11 @@ let main () = | [_; "references"; path; line; col] -> Commands.references ~path ~pos:(int_of_string line, int_of_string col) - ~debug:false + ~debug | [_; "rename"; path; line; col; newName] -> Commands.rename ~path ~pos:(int_of_string line, int_of_string col) - ~newName ~debug:false + ~newName ~debug | [_; "semanticTokens"; currentFile] -> SemanticTokens.semanticTokens ~currentFile | [_; "createInterface"; path; cmiFile] -> diff --git a/analysis/src/Debug.ml b/analysis/src/Debug.ml new file mode 100644 index 000000000..383ba1f67 --- /dev/null +++ b/analysis/src/Debug.ml @@ -0,0 +1,10 @@ +type debugLevel = Off | Regular | Verbose + +let debugLevel = ref Off + +let log s = + match !debugLevel with + | Regular | Verbose -> print_endline s + | Off -> () + +let logVerbose s = if !debugLevel = Verbose then print_endline s diff --git a/client/src/commands.ts b/client/src/commands.ts index 98bcec00d..250e47a11 100644 --- a/client/src/commands.ts +++ b/client/src/commands.ts @@ -8,6 +8,7 @@ import { 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 const codeAnalysisWithReanalyze = ( targetDir: string | null, diff --git a/client/src/commands/code_analysis.ts b/client/src/commands/code_analysis.ts index d808be79a..557747882 100644 --- a/client/src/commands/code_analysis.ts +++ b/client/src/commands/code_analysis.ts @@ -1,5 +1,4 @@ import * as cp from "child_process"; -import * as fs from "fs"; import * as path from "path"; import { window, @@ -14,6 +13,7 @@ import { WorkspaceEdit, OutputChannel, } from "vscode"; +import { analysisProdPath, getAnalysisBinaryPath } from "../utils"; export type DiagnosticsResultCodeActionsMap = Map< string, @@ -127,36 +127,6 @@ let resultsToDiagnostics = ( }; }; -let platformDir = process.arch === "arm64" ? process.platform + process.arch : process.platform; - -let analysisDevPath = path.join( - path.dirname(__dirname), - "..", - "..", - "analysis", - "rescript-editor-analysis.exe" -); - -let analysisProdPath = path.join( - path.dirname(__dirname), - "..", - "..", - "server", - "analysis_binaries", - platformDir, - "rescript-editor-analysis.exe" -); - -let getAnalysisBinaryPath = (): string | null => { - if (fs.existsSync(analysisDevPath)) { - return analysisDevPath; - } else if (fs.existsSync(analysisProdPath)) { - return analysisProdPath; - } else { - return null; - } -}; - export const runCodeAnalysisWithReanalyze = ( targetDir: string | null, diagnosticsCollection: DiagnosticCollection, diff --git a/client/src/commands/dump_debug.ts b/client/src/commands/dump_debug.ts new file mode 100644 index 000000000..ff1e3cbd0 --- /dev/null +++ b/client/src/commands/dump_debug.ts @@ -0,0 +1,318 @@ +import * as cp from "child_process"; +import * as fs from "fs"; +import { + ExtensionContext, + StatusBarItem, + Uri, + ViewColumn, + window, +} from "vscode"; +import { createFileInTempDir, getAnalysisBinaryPath } from "../utils"; +import * as path from "path"; + +// Maps to Cli.ml +const debugCommands = [ + { command: "dumpAst" as const, title: "Dump the AST" }, + { command: "completion" as const, title: "Completion" }, + { command: "definition" as const, title: "Definition" }, + { command: "typeDefinition" as const, title: "Type Definition" }, + { command: "documentSymbol" as const, title: "Document Symbol" }, + { command: "hover" as const, title: "Hover" }, + { command: "signatureHelp" as const, title: "Signature Help" }, + { command: "inlayHint" as const, title: "Inlay Hint" }, + { command: "codeLens" as const, title: "Code Lens" }, + { command: "extractDocs" as const, title: "Extract Docs" }, + { command: "codeAction" as const, title: "Code Action" }, + { command: "codemod" as const, title: "Code Mod" }, + { command: "diagnosticSyntax" as const, title: "Diagnostic Syntax" }, + { command: "references" as const, title: "References" }, + { command: "rename" as const, title: "Rename" }, + { command: "semanticTokens" as const, title: "Semantic Tokens" }, + { command: "createInterface" as const, title: "Create Interface" }, + { command: "format" as const, title: "Format" }, +]; + +const logLevels = ["verbose" as const, "regular" as const, "off" as const]; + +function runDebugDump({ + binaryPath, + cwd, + cliOptions, +}: { + binaryPath: string; + cwd: string; + cliOptions: string[]; +}): Promise { + return new Promise((resolve) => { + let opts = [...cliOptions]; + window.showInformationMessage(JSON.stringify(opts)); + let p = cp.spawn(binaryPath, opts, { + cwd, + }); + + if (p.stdout == null) { + window.showErrorMessage("Something went wrong."); + resolve(null); + return; + } + + let data = ""; + + p.stdout.on("data", (d) => { + data += d; + }); + + p.stderr?.on("data", (e) => { + window.showErrorMessage( + `Something went wrong trying to run debug dump: '${e}'` + ); + resolve(e.toString()); + }); + + p.on("close", () => { + resolve(data); + }); + }); +} + +function runBsc({ + cwd, + cliOptions, +}: { + cwd: string; + cliOptions: string[]; +}): Promise { + return new Promise((resolve) => { + let opts = ["bsc", ...cliOptions]; + let p = cp.spawn("npx", opts, { + cwd, + }); + + if (p.stdout == null) { + window.showErrorMessage("Something went wrong."); + resolve(null); + return; + } + + let data = ""; + + p.stdout.on("data", (d) => { + data += d; + }); + + p.stderr?.on("data", (e) => { + data += e.toString(); + }); + + p.on("close", () => { + resolve(data); + }); + }); +} + +let rerunCommand: null | (() => Promise) = null; + +export const dumpDebugRetrigger = () => { + if (rerunCommand != null) { + rerunCommand(); + } +}; + +export const dumpDebug = async ( + context: ExtensionContext, + statusBarItem: StatusBarItem +) => { + const editor = window.activeTextEditor; + + if (!editor) { + return window.showInformationMessage("No active editor"); + } + + const { line, character } = editor.selection.active; + const { line: endLine, character: endChar } = editor.selection.end; + const filePath = editor.document.uri.fsPath; + + const binaryPath = getAnalysisBinaryPath(); + if (binaryPath === null) { + window.showErrorMessage("Binary executable not found."); + return; + } + + const callTypeTitle = await window.showQuickPick( + debugCommands.map((d) => d.title), + { + title: "Select call type", + } + ); + const callType = debugCommands.find((d) => d.title === callTypeTitle); + + if (callType == null) { + window.showErrorMessage(`Debug call type not found: "${callTypeTitle}"`); + return null; + } + + let logLevel = "verbose"; + + if (!["dumpAst"].includes(callType.command)) { + logLevel = await window.showQuickPick(logLevels, { + title: "Select log level", + }); + } + + const outputFile = createFileInTempDir(callType.title, ".txt"); + const document = window.activeTextEditor.document; + + const runCommand = async () => { + const extension = path.extname(filePath); + const currentFile = createFileInTempDir("current_file", extension); + + fs.writeFileSync(currentFile, document.getText()); + + if (["dumpAst"].includes(callType.command)) { + switch (callType.command) { + case "dumpAst": + const res = await runBsc({ + cwd: path.dirname(filePath), + cliOptions: [ + "-dparsetree", + "-only-parse", + "-ignore-parse-errors", + "-bs-loc", + currentFile, + ], + }); + fs.writeFileSync(outputFile, `Pos: ${line}:${character}\n\n${res}`); + return; + } + + fs.rmSync(currentFile); + } + + const opts: string[] = ["debug-dump", logLevel, callType.command]; + + switch (callType.command) { + case "completion": { + opts.push( + filePath, + line.toString(), + character.toString(), + currentFile, + "true" + ); + break; + } + case "definition": { + opts.push(filePath, line.toString(), character.toString()); + break; + } + case "typeDefinition": { + opts.push(filePath, line.toString(), character.toString()); + break; + } + case "documentSymbol": { + opts.push(filePath); + break; + } + case "hover": { + opts.push( + filePath, + line.toString(), + character.toString(), + currentFile, + "true" + ); + break; + } + case "signatureHelp": { + opts.push(filePath, line.toString(), character.toString(), currentFile); + break; + } + case "inlayHint": { + window.showErrorMessage("Not implemented yet."); + return null; + } + case "codeLens": { + opts.push(filePath); + break; + } + case "extractDocs": { + opts.push(filePath); + break; + } + case "codeAction": { + opts.push( + filePath, + line.toString(), + character.toString(), + endLine.toString(), + endChar.toString(), + currentFile + ); + break; + } + case "codemod": { + opts.push( + currentFile, + line.toString(), + character.toString(), + "add-missing-cases" // TODO: Make selectable + ); + break; + } + case "diagnosticSyntax": { + opts.push(currentFile); + break; + } + case "references": { + opts.push(filePath, line.toString(), character.toString()); + break; + } + case "semanticTokens": { + opts.push(currentFile); + break; + } + case "createInterface": { + window.showErrorMessage("Not implemented yet."); + return null; + } + case "format": { + opts.push(currentFile); + break; + } + default: + window.showErrorMessage(`"${callType.title}" is not implemented yet.`); + return null; + } + + const res = await runDebugDump({ + binaryPath, + cwd: path.dirname(filePath), + cliOptions: opts, + }); + + fs.writeFileSync(outputFile, res); + fs.rmSync(currentFile); + }; + + rerunCommand = runCommand; + + await runCommand(); + + await window.showTextDocument(Uri.parse(outputFile), { + viewColumn: ViewColumn.Beside, + }); + + statusBarItem.show(); + statusBarItem.text = "$(debug-restart) Rerun command"; + statusBarItem.command = "rescript-vscode.debug-dump-retrigger"; + + const unwatch = fs.watch(outputFile, (event, _) => { + if (event === "rename") { + fs.rmSync(outputFile); + statusBarItem.hide(); + rerunCommand = null; + } + }); + + context.subscriptions.push({ dispose: () => unwatch }); +}; diff --git a/client/src/extension.ts b/client/src/extension.ts index ada9f360c..5e34dae37 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -173,6 +173,10 @@ export function activate(context: ExtensionContext) { StatusBarAlignment.Right ); + let debugDumpStatusBarItem = window.createStatusBarItem( + StatusBarAlignment.Right + ); + let inCodeAnalysisState: { active: boolean; activatedFromDirectory: string | null; @@ -203,6 +207,13 @@ export function activate(context: ExtensionContext) { customCommands.openCompiled(client); }); + commands.registerCommand("rescript-vscode.debug-dump-start", () => { + customCommands.dumpDebug(context, debugDumpStatusBarItem); + }); + + commands.registerCommand("rescript-vscode.debug-dump-retrigger", () => { + customCommands.dumpDebugRetrigger(); + }); commands.registerCommand( "rescript-vscode.go_to_location", diff --git a/client/src/utils.ts b/client/src/utils.ts new file mode 100644 index 000000000..0c6ba3ea7 --- /dev/null +++ b/client/src/utils.ts @@ -0,0 +1,41 @@ +import * as path from "path"; +import * as fs from "fs"; +import * as os from "os"; + +const platformDir = + process.arch === "arm64" ? process.platform + process.arch : process.platform; + +const analysisDevPath = path.join( + path.dirname(__dirname), + "..", + "analysis", + "rescript-editor-analysis.exe" +); + +export const analysisProdPath = path.join( + path.dirname(__dirname), + "..", + "server", + "analysis_binaries", + platformDir, + "rescript-editor-analysis.exe" +); + +export const getAnalysisBinaryPath = (): string | null => { + if (fs.existsSync(analysisDevPath)) { + return analysisDevPath; + } else if (fs.existsSync(analysisProdPath)) { + return analysisProdPath; + } else { + return null; + } +}; + +let tempFilePrefix = "rescript_" + process.pid + "_"; +let tempFileId = 0; + +export const createFileInTempDir = (prefix = "", extension = "") => { + let tempFileName = prefix + "_" + tempFilePrefix + tempFileId + extension; + tempFileId = tempFileId + 1; + return path.join(os.tmpdir(), tempFileName); +}; diff --git a/package.json b/package.json index b84fa13f1..9484d85e3 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,10 @@ "light": "assets/switch-impl-intf-light.svg", "dark": "assets/switch-impl-intf-dark.svg" } + }, + { + "command": "rescript-vscode.debug-dump-start", + "title": "DEBUG ReScript: Dump analysis info" } ], "keybindings": [