From 0e6e8b58f72b22b10195e484a91f1a7315d6b031 Mon Sep 17 00:00:00 2001 From: Stanley Horwood Date: Fri, 1 May 2026 09:45:59 +0200 Subject: [PATCH] feat(lsp): add `debugOverrideCommand` option Closes rust-lang/rust-analyzer#22128 --- crates/rust-analyzer/src/config.rs | 36 +++++++++ crates/rust-analyzer/src/handlers/request.rs | 2 + crates/rust-analyzer/src/lsp/ext.rs | 9 +++ crates/rust-analyzer/src/lsp/to_proto.rs | 34 ++++++++- crates/rust-analyzer/src/target_spec.rs | 56 +++++++++++++- crates/rust-analyzer/tests/slow-tests/main.rs | 76 +++++++++++++++++++ docs/book/src/configuration_generated.md | 36 +++++++++ docs/book/src/contributing/lsp-extensions.md | 9 ++- editors/code/package.json | 32 ++++++++ editors/code/src/debug.ts | 24 +++++- editors/code/src/lsp_ext.ts | 5 +- editors/code/src/run.ts | 9 ++- 12 files changed, 320 insertions(+), 8 deletions(-) diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs index 3a88a8fe8480..8c8fd30ced7f 100644 --- a/crates/rust-analyzer/src/config.rs +++ b/crates/rust-analyzer/src/config.rs @@ -957,6 +957,19 @@ config_data! { /// Subcommand used for bench runnables instead of `bench`. runnables_bench_command: String = "bench".to_owned(), + /// Override the command used for debugging bench runnables. + /// The first element of the array should be the program to execute (for example, `cargo`). + /// + /// Use the placeholders: + /// - `${package}`: package name. + /// - `${target_arg}`: target option such as `--bin`, `--test`, `--lib`, etc. + /// - `${target}`: target name (empty for `--lib`). + /// - `${test_name}`: the test path filter, e.g. `module::bench_func`. + /// - `${exact}`: `--exact` for single benchmarks, empty for modules. + /// - `${include_ignored}`: always empty for benchmarks. + /// - `${executable_args}`: all of the above binary args bundled together + /// (includes `rust-analyzer.runnables.extraTestBinaryArgs`). + runnables_bench_debugOverrideCommand: Option> = None, /// Override the command used for bench runnables. /// The first element of the array should be the program to execute (for example, `cargo`). /// @@ -998,6 +1011,19 @@ config_data! { runnables_extraTestBinaryArgs: Vec = vec!["--nocapture".to_owned()], /// Subcommand used for test runnables instead of `test`. runnables_test_command: String = "test".to_owned(), + /// Override the command used for debugging test runnables. + /// The first element of the array should be the program to execute (for example, `cargo`). + /// + /// Available placeholders: + /// - `${package}`: package name. + /// - `${target_arg}`: target option such as `--bin`, `--test`, `--lib`, etc. + /// - `${target}`: target name (empty for `--lib`). + /// - `${test_name}`: the test path filter, e.g. `module::test_func`. + /// - `${exact}`: `--exact` for single tests, empty for modules. + /// - `${include_ignored}`: `--include-ignored` for single tests, empty otherwise. + /// - `${executable_args}`: all of the above binary args bundled together + /// (includes `rust-analyzer.runnables.extraTestBinaryArgs`). + runnables_test_debugOverrideCommand: Option> = None, /// Override the command used for test runnables. /// The first element of the array should be the program to execute (for example, `cargo`). /// @@ -1679,10 +1705,14 @@ pub struct RunnablesConfig { pub test_command: String, /// Override the command used for test runnables. pub test_override_command: Option>, + /// Override the command used for debugging test runnables. + pub test_debug_override_command: Option>, /// Subcommand used for doctest runnables instead of `bench`. pub bench_command: String, /// Override the command used for bench runnables. pub bench_override_command: Option>, + /// Override the command used for debugging bench runnables. + pub bench_debug_override_command: Option>, /// Override the command used for doctest runnables. pub doc_test_override_command: Option>, } @@ -2642,8 +2672,14 @@ impl Config { extra_test_binary_args: self.runnables_extraTestBinaryArgs(source_root).clone(), test_command: self.runnables_test_command(source_root).clone(), test_override_command: self.runnables_test_overrideCommand(source_root).clone(), + test_debug_override_command: self + .runnables_test_debugOverrideCommand(source_root) + .clone(), bench_command: self.runnables_bench_command(source_root).clone(), bench_override_command: self.runnables_bench_overrideCommand(source_root).clone(), + bench_debug_override_command: self + .runnables_bench_debugOverrideCommand(source_root) + .clone(), doc_test_override_command: self.runnables_doctest_overrideCommand(source_root).clone(), } } diff --git a/crates/rust-analyzer/src/handlers/request.rs b/crates/rust-analyzer/src/handlers/request.rs index 0c1c067ffa91..33f88371df2e 100644 --- a/crates/rust-analyzer/src/handlers/request.rs +++ b/crates/rust-analyzer/src/handlers/request.rs @@ -1063,6 +1063,7 @@ pub(crate) fn handle_runnables( .into_iter() .collect(), }), + debug: None, }) } } @@ -1085,6 +1086,7 @@ pub(crate) fn handle_runnables( executable_args: Vec::new(), environment: Default::default(), }), + debug: None, }); }; } diff --git a/crates/rust-analyzer/src/lsp/ext.rs b/crates/rust-analyzer/src/lsp/ext.rs index 5d0d9209de2f..e4cdaefff1f2 100644 --- a/crates/rust-analyzer/src/lsp/ext.rs +++ b/crates/rust-analyzer/src/lsp/ext.rs @@ -453,6 +453,15 @@ pub struct Runnable { pub location: Option, pub kind: RunnableKind, pub args: RunnableArgs, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub debug: Option, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RunnableCommand { + pub kind: RunnableKind, + pub args: RunnableArgs, } #[derive(Deserialize, Serialize, Debug, Clone)] diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs index d59e9d8434c7..d9b4a68a5e0d 100644 --- a/crates/rust-analyzer/src/lsp/to_proto.rs +++ b/crates/rust-analyzer/src/lsp/to_proto.rs @@ -1597,11 +1597,21 @@ pub(crate) fn runnable( let label = runnable.label(Some(&target)); let location = location_link(snap, None, runnable.nav)?; - let environment = spec + let environment: rustc_hash::FxHashMap<_, _> = spec .sysroot_root + .as_ref() .map(|root| ("RUSTC_TOOLCHAIN".to_owned(), root.to_string())) .into_iter() .collect(); + let debug = + CargoTargetSpec::debug_override_command(snap, Some(spec.clone()), &runnable.kind) + .and_then(|override_command| { + shell_runnable_command( + override_command, + environment.clone(), + cwd.clone().into(), + ) + }); Ok(match override_command { Some(override_command) => match override_command.split_first() { @@ -1615,6 +1625,7 @@ pub(crate) fn runnable( program: program.to_string(), args: args.to_vec(), }), + debug, }), _ => None, }, @@ -1630,6 +1641,7 @@ pub(crate) fn runnable( executable_args, environment, }), + debug, }), }) } @@ -1650,6 +1662,7 @@ pub(crate) fn runnable( location: Some(location), kind: lsp_ext::RunnableKind::Shell, args: lsp_ext::RunnableArgs::Shell(runnable_args), + debug: None, })) } None => Ok(None), @@ -1677,11 +1690,29 @@ pub(crate) fn runnable( executable_args, environment: Default::default(), }), + debug: None, })) } } } +fn shell_runnable_command( + override_command: Vec, + environment: rustc_hash::FxHashMap, + cwd: paths::Utf8PathBuf, +) -> Option { + let (program, args) = override_command.split_first()?; + Some(lsp_ext::RunnableCommand { + kind: lsp_ext::RunnableKind::Shell, + args: lsp_ext::RunnableArgs::Shell(lsp_ext::ShellRunnableArgs { + environment, + cwd, + program: program.to_string(), + args: args.to_vec(), + }), + }) +} + pub(crate) fn code_lens( acc: &mut Vec, snap: &GlobalStateSnapshot, @@ -1984,6 +2015,7 @@ pub(crate) fn make_update_runnable( let mut runnable = runnable.clone(); runnable.label = format!("{} + {}", runnable.label, label); + runnable.debug = None; let lsp_ext::RunnableArgs::Cargo(r) = &mut runnable.args else { return None; diff --git a/crates/rust-analyzer/src/target_spec.rs b/crates/rust-analyzer/src/target_spec.rs index 5bdc9d8ca315..1881df3484a4 100644 --- a/crates/rust-analyzer/src/target_spec.rs +++ b/crates/rust-analyzer/src/target_spec.rs @@ -221,13 +221,64 @@ impl CargoTargetSpec { RunnableKind::DocTest { test_id } => { (config.doc_test_override_command, Some(test_id.to_string())) } - RunnableKind::Bin => match spec { + RunnableKind::Bin => match &spec { Some(CargoTargetSpec { target_kind: TargetKind::Test, .. }) => { (config.test_override_command, None) } _ => (None, None), }, }; + + Self::expand_override_command( + spec.as_ref(), + kind, + args, + test_name, + config.extra_test_binary_args, + ) + } + + pub(crate) fn debug_override_command( + snap: &GlobalStateSnapshot, + spec: Option, + kind: &RunnableKind, + ) -> Option> { + let config = snap.config.runnables(None); + let (args, test_name) = match kind { + RunnableKind::Test { test_id, .. } => { + (config.test_debug_override_command, Some(test_id.to_string())) + } + RunnableKind::TestMod { path } => { + (config.test_debug_override_command, Some(path.clone())) + } + RunnableKind::Bench { test_id } => { + (config.bench_debug_override_command, Some(test_id.to_string())) + } + RunnableKind::Bin => match &spec { + Some(CargoTargetSpec { target_kind: TargetKind::Test, .. }) => { + (config.test_debug_override_command, None) + } + _ => (None, None), + }, + _ => (None, None), + }; + + Self::expand_override_command( + spec.as_ref(), + kind, + args, + test_name, + config.extra_test_binary_args, + ) + } + + fn expand_override_command( + spec: Option<&CargoTargetSpec>, + kind: &RunnableKind, + args: Option>, + test_name: Option, + extra_test_binary_args: Vec, + ) -> Option> { let test_name = test_name.unwrap_or_default(); let exact = match kind { @@ -256,7 +307,7 @@ impl CargoTargetSpec { _ => "", }; - let replace_placeholders = |arg: String| match &spec { + let replace_placeholders = |arg: String| match spec { Some(spec) => arg .replace("${package}", &spec.package) .replace("${target_arg}", target_arg(spec.target_kind)) @@ -267,7 +318,6 @@ impl CargoTargetSpec { _ => arg, }; - let extra_test_binary_args = config.extra_test_binary_args; let executable_args = Self::executable_args_for(kind, extra_test_binary_args); args.map(|mut args| { diff --git a/crates/rust-analyzer/tests/slow-tests/main.rs b/crates/rust-analyzer/tests/slow-tests/main.rs index a8632630784b..d0ae698d5b5f 100644 --- a/crates/rust-analyzer/tests/slow-tests/main.rs +++ b/crates/rust-analyzer/tests/slow-tests/main.rs @@ -350,6 +350,82 @@ fn main() {} ); } +#[test] +fn test_runnables_debug_override_command() { + if skip_slow_tests() { + return; + } + + let server = Project::with_fixture( + r#" +//- /foo/Cargo.toml +[package] +name = "foo" +version = "0.0.0" + +//- /foo/tests/spam.rs +#[test] +fn test_eggs() {} +"#, + ) + .with_config(json!({ + "runnables": { + "test": { + "overrideCommand": [ + "cargo", "nextest", "run", + "--package", "${package}", "${target_arg}", "${target}", + "--", "${executable_args}", + ], + "debugOverrideCommand": [ + "cargo", "nextest", "run", + "--debugger", "codelldb-launch --", + "--package", "${package}", "${target_arg}", "${target}", + "--", "${executable_args}", + ], + }, + }, + })) + .root("foo") + .server() + .wait_until_workspace_is_loaded(); + + server.request::( + RunnablesParams { text_document: server.doc_id("foo/tests/spam.rs"), position: None }, + json!([ + { + "args": { + "args": [ + "nextest", "run", + "--package", "foo", "--test", "spam", + "--", "test_eggs", "--exact", "--nocapture", "--include-ignored", + ], + "cwd": server.path().join("foo"), + "program": "cargo", + }, + "debug": { + "args": { + "args": [ + "nextest", "run", + "--debugger", "codelldb-launch --", + "--package", "foo", "--test", "spam", + "--", "test_eggs", "--exact", "--nocapture", "--include-ignored", + ], + "cwd": server.path().join("foo"), + "program": "cargo", + }, + "kind": "shell", + }, + "kind": "shell", + "label": "test test_eggs", + "location": "{...}", + }, + "{...}", + "{...}", + "{...}", + ]), + ); +} + // Each package in these workspaces should be run from its own root #[test] fn test_path_dependency_runnables() { diff --git a/docs/book/src/configuration_generated.md b/docs/book/src/configuration_generated.md index da37fc158234..beb5309764ef 100644 --- a/docs/book/src/configuration_generated.md +++ b/docs/book/src/configuration_generated.md @@ -1397,6 +1397,24 @@ Default: `"bench"` Subcommand used for bench runnables instead of `bench`. +## rust-analyzer.runnables.bench.debugOverrideCommand {#runnables.bench.debugOverrideCommand} + +Default: `null` + +Override the command used for debugging bench runnables. +The first element of the array should be the program to execute (for example, `cargo`). + +Use the placeholders: +- `${package}`: package name. +- `${target_arg}`: target option such as `--bin`, `--test`, `--lib`, etc. +- `${target}`: target name (empty for `--lib`). +- `${test_name}`: the test path filter, e.g. `module::bench_func`. +- `${exact}`: `--exact` for single benchmarks, empty for modules. +- `${include_ignored}`: always empty for benchmarks. +- `${executable_args}`: all of the above binary args bundled together + (includes `rust-analyzer.runnables.extraTestBinaryArgs`). + + ## rust-analyzer.runnables.bench.overrideCommand {#runnables.bench.overrideCommand} Default: `null` @@ -1473,6 +1491,24 @@ Default: `"test"` Subcommand used for test runnables instead of `test`. +## rust-analyzer.runnables.test.debugOverrideCommand {#runnables.test.debugOverrideCommand} + +Default: `null` + +Override the command used for debugging test runnables. +The first element of the array should be the program to execute (for example, `cargo`). + +Available placeholders: +- `${package}`: package name. +- `${target_arg}`: target option such as `--bin`, `--test`, `--lib`, etc. +- `${target}`: target name (empty for `--lib`). +- `${test_name}`: the test path filter, e.g. `module::test_func`. +- `${exact}`: `--exact` for single tests, empty for modules. +- `${include_ignored}`: `--include-ignored` for single tests, empty otherwise. +- `${executable_args}`: all of the above binary args bundled together + (includes `rust-analyzer.runnables.extraTestBinaryArgs`). + + ## rust-analyzer.runnables.test.overrideCommand {#runnables.test.overrideCommand} Default: `null` diff --git a/docs/book/src/contributing/lsp-extensions.md b/docs/book/src/contributing/lsp-extensions.md index 8ba6f6ab531e..e0dbcf0e7a42 100644 --- a/docs/book/src/contributing/lsp-extensions.md +++ b/docs/book/src/contributing/lsp-extensions.md @@ -1,5 +1,5 @@