diff --git a/crates/rust-analyzer/src/main_loop/handlers.rs b/crates/rust-analyzer/src/main_loop/handlers.rs index 8d7baa14f94..6b14830b6df 100644 --- a/crates/rust-analyzer/src/main_loop/handlers.rs +++ b/crates/rust-analyzer/src/main_loop/handlers.rs @@ -1011,6 +1011,7 @@ fn to_lsp_runnable( runnable: Runnable, ) -> Result { let spec = CargoTargetSpec::for_file(world, file_id)?; + let target = spec.as_ref().map(|s| s.target.clone()); let (args, extra_args) = CargoTargetSpec::runnable_args(spec, &runnable.kind)?; let line_index = world.analysis().file_line_index(file_id)?; let label = match &runnable.kind { @@ -1018,7 +1019,9 @@ fn to_lsp_runnable( RunnableKind::TestMod { path } => format!("test-mod {}", path), RunnableKind::Bench { test_id } => format!("bench {}", test_id), RunnableKind::DocTest { test_id, .. } => format!("doctest {}", test_id), - RunnableKind::Bin => "run binary".to_string(), + RunnableKind::Bin => { + target.map_or_else(|| "run binary".to_string(), |t| format!("run {}", t)) + } }; Ok(lsp_ext::Runnable { range: to_proto::range(&line_index, runnable.range), diff --git a/editors/code/package.json b/editors/code/package.json index b8a2182f0fd..4e7e3faf700 100644 --- a/editors/code/package.json +++ b/editors/code/package.json @@ -120,6 +120,16 @@ "title": "Run", "category": "Rust Analyzer" }, + { + "command": "rust-analyzer.debug", + "title": "Debug", + "category": "Rust Analyzer" + }, + { + "command": "rust-analyzer.newDebugConfig", + "title": "Generate launch configuration", + "category": "Rust Analyzer" + }, { "command": "rust-analyzer.analyzerStatus", "title": "Status", diff --git a/editors/code/src/cargo.ts b/editors/code/src/cargo.ts index 2a2c2e0e1bb..28c7de992b4 100644 --- a/editors/code/src/cargo.ts +++ b/editors/code/src/cargo.ts @@ -49,7 +49,20 @@ export class Cargo { async executableFromArgs(args: readonly string[]): Promise { const cargoArgs = [...args, "--message-format=json"]; - const artifacts = await this.artifactsFromArgs(cargoArgs); + // arguments for a runnable from the quick pick should be updated. + // see crates\rust-analyzer\src\main_loop\handlers.rs, handle_code_lens + if (cargoArgs[0] === "run") { + cargoArgs[0] = "build"; + } else if (cargoArgs.indexOf("--no-run") === -1) { + cargoArgs.push("--no-run"); + } + + let artifacts = await this.artifactsFromArgs(cargoArgs); + if (cargoArgs[0] === "test") { + // for instance, `crates\rust-analyzer\tests\heavy_tests\main.rs` tests + // produce 2 artifacts: {"kind": "bin"} and {"kind": "test"} + artifacts = artifacts.filter(a => a.isTest); + } if (artifacts.length === 0) { throw new Error('No compilation artifacts'); diff --git a/editors/code/src/commands/runnables.ts b/editors/code/src/commands/runnables.ts index ae328d2a427..b1d93fc34eb 100644 --- a/editors/code/src/commands/runnables.ts +++ b/editors/code/src/commands/runnables.ts @@ -1,43 +1,82 @@ import * as vscode from 'vscode'; import * as lc from 'vscode-languageclient'; import * as ra from '../rust-analyzer-api'; -import * as os from "os"; import { Ctx, Cmd } from '../ctx'; -import { Cargo } from '../cargo'; +import { startDebugSession, getDebugConfiguration } from '../debug'; + +const quickPickButtons = [{ iconPath: new vscode.ThemeIcon("save"), tooltip: "Save as a launch.json configurtation." }]; + +async function selectRunnable(ctx: Ctx, prevRunnable?: RunnableQuickPick, showButtons: boolean = true): Promise { + const editor = ctx.activeRustEditor; + const client = ctx.client; + if (!editor || !client) return; + + const textDocument: lc.TextDocumentIdentifier = { + uri: editor.document.uri.toString(), + }; + + const runnables = await client.sendRequest(ra.runnables, { + textDocument, + position: client.code2ProtocolConverter.asPosition( + editor.selection.active, + ), + }); + const items: RunnableQuickPick[] = []; + if (prevRunnable) { + items.push(prevRunnable); + } + for (const r of runnables) { + if ( + prevRunnable && + JSON.stringify(prevRunnable.runnable) === JSON.stringify(r) + ) { + continue; + } + items.push(new RunnableQuickPick(r)); + } + + return await new Promise((resolve) => { + const disposables: vscode.Disposable[] = []; + const close = (result?: RunnableQuickPick) => { + resolve(result); + disposables.forEach(d => d.dispose()); + }; + + const quickPick = vscode.window.createQuickPick(); + quickPick.items = items; + quickPick.title = "Select Runnable"; + if (showButtons) { + quickPick.buttons = quickPickButtons; + } + disposables.push( + quickPick.onDidHide(() => close()), + quickPick.onDidAccept(() => close(quickPick.selectedItems[0])), + quickPick.onDidTriggerButton((_button) => { + (async () => await makeDebugConfig(ctx, quickPick.activeItems[0]))(); + close(); + }), + quickPick.onDidChangeActive((active) => { + if (showButtons && active.length > 0) { + if (active[0].label.startsWith('cargo')) { + // save button makes no sense for `cargo test` or `cargo check` + quickPick.buttons = []; + } else if (quickPick.buttons.length === 0) { + quickPick.buttons = quickPickButtons; + } + } + }), + quickPick + ); + quickPick.show(); + }); +} export function run(ctx: Ctx): Cmd { let prevRunnable: RunnableQuickPick | undefined; return async () => { - const editor = ctx.activeRustEditor; - const client = ctx.client; - if (!editor || !client) return; - - const textDocument: lc.TextDocumentIdentifier = { - uri: editor.document.uri.toString(), - }; - - const runnables = await client.sendRequest(ra.runnables, { - textDocument, - position: client.code2ProtocolConverter.asPosition( - editor.selection.active, - ), - }); - const items: RunnableQuickPick[] = []; - if (prevRunnable) { - items.push(prevRunnable); - } - for (const r of runnables) { - if ( - prevRunnable && - JSON.stringify(prevRunnable.runnable) === JSON.stringify(r) - ) { - continue; - } - items.push(new RunnableQuickPick(r)); - } - const item = await vscode.window.showQuickPick(items); + const item = await selectRunnable(ctx, prevRunnable); if (!item) return; item.detail = 'rerun'; @@ -64,88 +103,54 @@ export function runSingle(ctx: Ctx): Cmd { }; } -function getLldbDebugConfig(config: ra.Runnable, executable: string, sourceFileMap?: Record): vscode.DebugConfiguration { - return { - type: "lldb", - request: "launch", - name: config.label, - program: executable, - args: config.extraArgs, - cwd: config.cwd, - sourceMap: sourceFileMap, - sourceLanguages: ["rust"] +export function debug(ctx: Ctx): Cmd { + let prevDebuggee: RunnableQuickPick | undefined; + + return async () => { + const item = await selectRunnable(ctx, prevDebuggee); + if (!item) return; + + item.detail = 'restart'; + prevDebuggee = item; + return await startDebugSession(ctx, item.runnable); }; } -function getCppvsDebugConfig(config: ra.Runnable, executable: string, sourceFileMap?: Record): vscode.DebugConfiguration { - return { - type: (os.platform() === "win32") ? "cppvsdbg" : 'cppdbg', - request: "launch", - name: config.label, - program: executable, - args: config.extraArgs, - cwd: config.cwd, - sourceFileMap: sourceFileMap, - }; -} - -const debugOutput = vscode.window.createOutputChannel("Debug"); - -async function getDebugExecutable(config: ra.Runnable): Promise { - const cargo = new Cargo(config.cwd || '.', debugOutput); - const executable = await cargo.executableFromArgs(config.args); - - // if we are here, there were no compilation errors. - return executable; -} - -type DebugConfigProvider = (config: ra.Runnable, executable: string, sourceFileMap?: Record) => vscode.DebugConfiguration; - export function debugSingle(ctx: Ctx): Cmd { return async (config: ra.Runnable) => { - const editor = ctx.activeRustEditor; - if (!editor) return; + await startDebugSession(ctx, config); + }; +} - const knownEngines: Record = { - "vadimcn.vscode-lldb": getLldbDebugConfig, - "ms-vscode.cpptools": getCppvsDebugConfig - }; - const debugOptions = ctx.config.debug; +async function makeDebugConfig(ctx: Ctx, item: RunnableQuickPick): Promise { + const scope = ctx.activeRustEditor?.document.uri; + if (!scope) return; - let debugEngine = null; - if (debugOptions.engine === "auto") { - for (var engineId in knownEngines) { - debugEngine = vscode.extensions.getExtension(engineId); - if (debugEngine) break; - } - } - else { - debugEngine = vscode.extensions.getExtension(debugOptions.engine); - } + const debugConfig = await getDebugConfiguration(ctx, item.runnable); + if (!debugConfig) return; - if (!debugEngine) { - vscode.window.showErrorMessage(`Install [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)` - + ` or [MS C++ tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension for debugging.`); - return; - } + const wsLaunchSection = vscode.workspace.getConfiguration("launch", scope); + const configurations = wsLaunchSection.get("configurations") || []; - debugOutput.clear(); - if (ctx.config.debug.openUpDebugPane) { - debugOutput.show(true); - } + const index = configurations.findIndex(c => c.name === debugConfig.name); + if (index !== -1) { + const answer = await vscode.window.showErrorMessage(`Launch configuration '${debugConfig.name}' already exists!`, 'Cancel', 'Update'); + if (answer === "Cancel") return; - const executable = await getDebugExecutable(config); - const debugConfig = knownEngines[debugEngine.id](config, executable, debugOptions.sourceFileMap); - if (debugConfig.type in debugOptions.engineSettings) { - const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type]; - for (var key in settingsMap) { - debugConfig[key] = settingsMap[key]; - } - } + configurations[index] = debugConfig; + } else { + configurations.push(debugConfig); + } - debugOutput.appendLine("Launching debug configuration:"); - debugOutput.appendLine(JSON.stringify(debugConfig, null, 2)); - return vscode.debug.startDebugging(undefined, debugConfig); + await wsLaunchSection.update("configurations", configurations); +} + +export function newDebugConfig(ctx: Ctx): Cmd { + return async () => { + const item = await selectRunnable(ctx, undefined, false); + if (!item) return; + + await makeDebugConfig(ctx, item); }; } diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index be2e27aecca..1652827c32a 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -116,7 +116,7 @@ export class Config { engine: this.get("debug.engine"), engineSettings: this.get("debug.engineSettings"), openUpDebugPane: this.get("debug.openUpDebugPane"), - sourceFileMap: sourceFileMap, + sourceFileMap: sourceFileMap }; } } diff --git a/editors/code/src/debug.ts b/editors/code/src/debug.ts new file mode 100644 index 00000000000..d3fe588e831 --- /dev/null +++ b/editors/code/src/debug.ts @@ -0,0 +1,124 @@ +import * as os from "os"; +import * as vscode from 'vscode'; +import * as path from 'path'; +import * as ra from './rust-analyzer-api'; + +import { Cargo } from './cargo'; +import { Ctx } from "./ctx"; + +const debugOutput = vscode.window.createOutputChannel("Debug"); +type DebugConfigProvider = (config: ra.Runnable, executable: string, sourceFileMap?: Record) => vscode.DebugConfiguration; + +function getLldbDebugConfig(config: ra.Runnable, executable: string, sourceFileMap?: Record): vscode.DebugConfiguration { + return { + type: "lldb", + request: "launch", + name: config.label, + program: executable, + args: config.extraArgs, + cwd: config.cwd, + sourceMap: sourceFileMap, + sourceLanguages: ["rust"] + }; +} + +function getCppvsDebugConfig(config: ra.Runnable, executable: string, sourceFileMap?: Record): vscode.DebugConfiguration { + return { + type: (os.platform() === "win32") ? "cppvsdbg" : "cppdbg", + request: "launch", + name: config.label, + program: executable, + args: config.extraArgs, + cwd: config.cwd, + sourceFileMap: sourceFileMap, + }; +} + +async function getDebugExecutable(config: ra.Runnable): Promise { + const cargo = new Cargo(config.cwd || '.', debugOutput); + const executable = await cargo.executableFromArgs(config.args); + + // if we are here, there were no compilation errors. + return executable; +} + +export async function getDebugConfiguration(ctx: Ctx, config: ra.Runnable): Promise { + const editor = ctx.activeRustEditor; + if (!editor) return; + + const knownEngines: Record = { + "vadimcn.vscode-lldb": getLldbDebugConfig, + "ms-vscode.cpptools": getCppvsDebugConfig + }; + const debugOptions = ctx.config.debug; + + let debugEngine = null; + if (debugOptions.engine === "auto") { + for (var engineId in knownEngines) { + debugEngine = vscode.extensions.getExtension(engineId); + if (debugEngine) break; + } + } else { + debugEngine = vscode.extensions.getExtension(debugOptions.engine); + } + + if (!debugEngine) { + vscode.window.showErrorMessage(`Install [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb)` + + ` or [MS C++ tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools) extension for debugging.`); + return; + } + + debugOutput.clear(); + if (ctx.config.debug.openUpDebugPane) { + debugOutput.show(true); + } + + const wsFolder = path.normalize(vscode.workspace.workspaceFolders![0].uri.fsPath); // folder exists or RA is not active. + function simplifyPath(p: string): string { + return path.normalize(p).replace(wsFolder, '${workspaceRoot}'); + } + + const executable = await getDebugExecutable(config); + const debugConfig = knownEngines[debugEngine.id](config, simplifyPath(executable), debugOptions.sourceFileMap); + if (debugConfig.type in debugOptions.engineSettings) { + const settingsMap = (debugOptions.engineSettings as any)[debugConfig.type]; + for (var key in settingsMap) { + debugConfig[key] = settingsMap[key]; + } + } + + if (debugConfig.name === "run binary") { + // The LSP side: crates\rust-analyzer\src\main_loop\handlers.rs, + // fn to_lsp_runnable(...) with RunnableKind::Bin + debugConfig.name = `run ${path.basename(executable)}`; + } + + if (debugConfig.cwd) { + debugConfig.cwd = simplifyPath(debugConfig.cwd); + } + + return debugConfig; +} + +export async function startDebugSession(ctx: Ctx, config: ra.Runnable): Promise { + let debugConfig: vscode.DebugConfiguration | undefined = undefined; + let message = ""; + + const wsLaunchSection = vscode.workspace.getConfiguration("launch"); + const configurations = wsLaunchSection.get("configurations") || []; + + const index = configurations.findIndex(c => c.name === config.label); + if (-1 !== index) { + debugConfig = configurations[index]; + message = " (from launch.json)"; + debugOutput.clear(); + } else { + debugConfig = await getDebugConfiguration(ctx, config); + } + + if (!debugConfig) return false; + + debugOutput.appendLine(`Launching debug configuration${message}:`); + debugOutput.appendLine(JSON.stringify(debugConfig, null, 2)); + return vscode.debug.startDebugging(undefined, debugConfig); +} diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 9b020d0019a..c015460b883 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -77,6 +77,8 @@ export async function activate(context: vscode.ExtensionContext) { ctx.registerCommand('syntaxTree', commands.syntaxTree); ctx.registerCommand('expandMacro', commands.expandMacro); ctx.registerCommand('run', commands.run); + ctx.registerCommand('debug', commands.debug); + ctx.registerCommand('newDebugConfig', commands.newDebugConfig); defaultOnEnter.dispose(); ctx.registerCommand('onEnter', commands.onEnter);