1import * as os from "os";
2import * as vscode from 'vscode';
3import * as path from 'path';
4import * as ra from './lsp_ext';
5
6import { Cargo, getRustcId, getSysroot } from './toolchain';
7import { Ctx } from "./ctx";
8import { prepareEnv } from "./run";
9
10const debugOutput = vscode.window.createOutputChannel("Debug");
11type DebugConfigProvider = (config: ra.Runnable, executable: string, env: Record<string, string>, sourceFileMap?: Record<string, string>) => vscode.DebugConfiguration;
12
13export async function makeDebugConfig(ctx: Ctx, runnable: ra.Runnable): Promise<void> {
14    const scope = ctx.activeRustEditor?.document.uri;
15    if (!scope) return;
16
17    const debugConfig = await getDebugConfiguration(ctx, runnable);
18    if (!debugConfig) return;
19
20    const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope);
21    const configurations = wsLaunchSection.get<any[]>("configurations") || [];
22
23    const index = configurations.findIndex(c => c.name === debugConfig.name);
24    if (index !== -1) {
25        const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update');
26        if (answer === "Cancel") return;
27
28        configurations[index] = debugConfig;
29    } else {
30        configurations.push(debugConfig);
31    }
32
33    await wsLaunchSection.update("configurations", configurations);
34}
35
36export async function startDebugSession(ctx: Ctx, runnable: ra.Runnable): Promise<boolean> {
37    let debugConfig: vscode.DebugConfiguration | undefined = undefined;
38    let message = "";
39
40    const wsLaunchSection = vscode.workspace.getConfiguration("launch");
41    const configurations = wsLaunchSection.get<any[]>("configurations") || [];
42
43    const index = configurations.findIndex(c => c.name === runnable.label);
44    if (-1 !== index) {
45        debugConfig = configurations[index];
46        message = " (from launch.json)";
47        debugOutput.clear();
48    } else {
49        debugConfig = await getDebugConfiguration(ctx, runnable);
50    }
51
52    if (!debugConfig) return false;
53
54    debugOutput.appendLine(`Launching debug configuration${message}:`);
55    debugOutput.appendLine(JSON.stringify(debugConfig, null, 2));
56    return vscode.debug.startDebugging(undefined, debugConfig);
57}
58
59async function getDebugConfiguration(ctx: Ctx, runnable: ra.Runnable): Promise<vscode.DebugConfiguration | undefined> {
60    const editor = ctx.activeRustEditor;
61    if (!editor) return;
62
63    const knownEngines: Record<string, DebugConfigProvider> = {
64        "vadimcn.vscode-lldb": getLldbDebugConfig,
65        "ms-vscode.cpptools": getCppvsDebugConfig
66    };
67    const debugOptions = ctx.config.debug;
68
69    let debugEngine = null;
70    if (debugOptions.engine === "auto") {
71        for (var engineId in knownEngines) {
72            debugEngine = vscode.extensions.getExtension(engineId);
73            if (debugEngine) break;
74        }
75    } else {
76        debugEngine = vscode.extensions.getExtension(debugOptions.engine);
77    }
78
79    if (!debugEngine) {
80        await vscode.window.showErrorMessage(`Install [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)`
81            + ` or [MS C++ tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension for debugging.`);
82        return;
83    }
84
85    debugOutput.clear();
86    if (ctx.config.debug.openDebugPane) {
87        debugOutput.show(true);
88    }
89    // folder exists or RA is not active.
90    // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
91    const workspaceFolders = vscode.workspace.workspaceFolders!;
92    const isMultiFolderWorkspace = workspaceFolders.length > 1;
93    const firstWorkspace = workspaceFolders[0];
94    const workspace = !isMultiFolderWorkspace || !runnable.args.workspaceRoot ?
95        firstWorkspace :
96        workspaceFolders.find(w => runnable.args.workspaceRoot?.includes(w.uri.fsPath)) || firstWorkspace;
97
98    const wsFolder = path.normalize(workspace.uri.fsPath);
99    const workspaceQualifier = isMultiFolderWorkspace ? `:${workspace.name}` : '';
100    function simplifyPath(p: string): string {
101        // see https://github.com/rust-analyzer/rust-analyzer/pull/5513#issuecomment-663458818 for why this is needed
102        return path.normalize(p).replace(wsFolder, '${workspaceFolder' + workspaceQualifier + '}');
103    }
104
105    const executable = await getDebugExecutable(runnable);
106    const env = prepareEnv(runnable, ctx.config.runnableEnv);
107    let sourceFileMap = debugOptions.sourceFileMap;
108    if (sourceFileMap === "auto") {
109        // let's try to use the default toolchain
110        const commitHash = await getRustcId(wsFolder);
111        const sysroot = await getSysroot(wsFolder);
112        const rustlib = path.normalize(sysroot + "/lib/rustlib/src/rust");
113        sourceFileMap = {};
114        sourceFileMap[`/rustc/${commitHash}/`] = rustlib;
115    }
116
117    const debugConfig = knownEngines[debugEngine.id](runnable, simplifyPath(executable), env, sourceFileMap);
118    if (debugConfig.type in debugOptions.engineSettings) {
119        const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type];
120        for (var key in settingsMap) {
121            debugConfig[key] = settingsMap[key];
122        }
123    }
124
125    if (debugConfig.name === "run binary") {
126        // The LSP side: crates\rust-analyzer\src\main_loop\handlers.rs,
127        // fn to_lsp_runnable(...) with RunnableKind::Bin
128        debugConfig.name = `run ${path.basename(executable)}`;
129    }
130
131    if (debugConfig.cwd) {
132        debugConfig.cwd = simplifyPath(debugConfig.cwd);
133    }
134
135    return debugConfig;
136}
137
138async function getDebugExecutable(runnable: ra.Runnable): Promise<string> {
139    const cargo = new Cargo(runnable.args.workspaceRoot || '.', debugOutput);
140    const executable = await cargo.executableFromArgs(runnable.args.cargoArgs);
141
142    // if we are here, there were no compilation errors.
143    return executable;
144}
145
146function getLldbDebugConfig(runnable: ra.Runnable, executable: string, env: Record<string, string>, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration {
147    return {
148        type: "lldb",
149        request: "launch",
150        name: runnable.label,
151        program: executable,
152        args: runnable.args.executableArgs,
153        cwd: runnable.args.workspaceRoot,
154        sourceMap: sourceFileMap,
155        sourceLanguages: ["rust"],
156        env
157    };
158}
159
160function getCppvsDebugConfig(runnable: ra.Runnable, executable: string, env: Record<string, string>, sourceFileMap?: Record<string, string>): vscode.DebugConfiguration {
161    return {
162        type: (os.platform() === "win32") ? "cppvsdbg" : "cppdbg",
163        request: "launch",
164        name: runnable.label,
165        program: executable,
166        args: runnable.args.executableArgs,
167        cwd: runnable.args.workspaceRoot,
168        sourceFileMap,
169        env,
170    };
171}
172