From c9bd4097328531db5d9be75f306bb79e1d4be6b6 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Mon, 23 Jun 2025 13:06:48 -0400 Subject: [PATCH] debugger: Support passing custom arguments to debug adapters (#33251) Custom arguments replace any arguments that we normally pass to the DAP. For interpreted languages, they are passed to the interpreter after the DAP path or module. They can be combined with a custom binary, or you can omit `dap.binary` and just customize the arguments to the DAPs we download. This doesn't take care of updating the extension API to support custom arguments. Release Notes: - debugger: Implemented support for passing custom arguments to a debug adapter binary using the `dap.args` setting. - debugger: Fixed not being able to use the `dap` setting in `.zed/settings.json`. --- crates/dap/src/adapters.rs | 2 + crates/dap_adapters/src/codelldb.rs | 11 ++- crates/dap_adapters/src/gdb.rs | 3 +- crates/dap_adapters/src/go.rs | 6 +- crates/dap_adapters/src/javascript.rs | 33 +++++-- crates/dap_adapters/src/php.rs | 39 ++++++-- crates/dap_adapters/src/python.rs | 93 ++++++++++++++----- crates/dap_adapters/src/ruby.rs | 1 + .../src/extension_dap_adapter.rs | 2 + crates/project/src/debugger/dap_store.rs | 16 +++- crates/project/src/project_settings.rs | 2 + 11 files changed, 154 insertions(+), 54 deletions(-) diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index a269c099cc5d6d1ad16e7d5a23ac56a5743631d7..8e1c84083f18835dee6c4bc3bea4ce7c45147499 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -344,6 +344,7 @@ pub trait DebugAdapter: 'static + Send + Sync { delegate: &Arc, config: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, cx: &mut AsyncApp, ) -> Result; @@ -434,6 +435,7 @@ impl DebugAdapter for FakeAdapter { _: &Arc, task_definition: &DebugTaskDefinition, _: Option, + _: Option>, _: &mut AsyncApp, ) -> Result { Ok(DebugAdapterBinary { diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index 31589966819ed35bbd6c71cd41124cc06bb25c94..5d14cc87475c814639ab8e15b54df46d9a01dd4c 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -329,6 +329,7 @@ impl DebugAdapter for CodeLldbDebugAdapter { delegate: &Arc, config: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, _: &mut AsyncApp, ) -> Result { let mut command = user_installed_path @@ -364,10 +365,12 @@ impl DebugAdapter for CodeLldbDebugAdapter { Ok(DebugAdapterBinary { command: Some(command.unwrap()), cwd: Some(delegate.worktree_root_path().to_path_buf()), - arguments: vec![ - "--settings".into(), - json!({"sourceLanguages": ["cpp", "rust"]}).to_string(), - ], + arguments: user_args.unwrap_or_else(|| { + vec![ + "--settings".into(), + json!({"sourceLanguages": ["cpp", "rust"]}).to_string(), + ] + }), request_args: self.request_args(delegate, &config).await?, envs: HashMap::default(), connection: None, diff --git a/crates/dap_adapters/src/gdb.rs b/crates/dap_adapters/src/gdb.rs index e889588359f594e16a450216690a2e8e974df236..17b7a659111532b5fa04f2b3424e50e7867df6d6 100644 --- a/crates/dap_adapters/src/gdb.rs +++ b/crates/dap_adapters/src/gdb.rs @@ -159,6 +159,7 @@ impl DebugAdapter for GdbDebugAdapter { delegate: &Arc, config: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, _: &mut AsyncApp, ) -> Result { let user_setting_path = user_installed_path @@ -186,7 +187,7 @@ impl DebugAdapter for GdbDebugAdapter { Ok(DebugAdapterBinary { command: Some(gdb_path), - arguments: vec!["-i=dap".into()], + arguments: user_args.unwrap_or_else(|| vec!["-i=dap".into()]), envs: HashMap::default(), cwd: Some(delegate.worktree_root_path().to_path_buf()), connection: None, diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index afd733b56a10161f808c6198d94485130ada83b5..bc3f5007454adee4cfcbc8a3cf09c87ae0100b97 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -399,6 +399,7 @@ impl DebugAdapter for GoDebugAdapter { delegate: &Arc, task_definition: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, _cx: &mut AsyncApp, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME); @@ -470,7 +471,10 @@ impl DebugAdapter for GoDebugAdapter { crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?; command = Some(minidelve_path.to_string_lossy().into_owned()); connection = None; - arguments = if cfg!(windows) { + arguments = if let Some(mut args) = user_args { + args.insert(0, delve_path); + args + } else if cfg!(windows) { vec![ delve_path, "dap".into(), diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index e59fb101ff8ceb764ae6a7977f3b146d8e390f6a..d5d78186acc9c76fc2dda5d096b099bd52aaf2a4 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -50,6 +50,7 @@ impl JsDebugAdapter { delegate: &Arc, task_definition: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, _: &mut AsyncApp, ) -> Result { let adapter_path = if let Some(user_installed_path) = user_installed_path { @@ -109,6 +110,26 @@ impl JsDebugAdapter { .or_insert(true.into()); } + let arguments = if let Some(mut args) = user_args { + args.insert( + 0, + adapter_path + .join(Self::ADAPTER_PATH) + .to_string_lossy() + .to_string(), + ); + args + } else { + vec![ + adapter_path + .join(Self::ADAPTER_PATH) + .to_string_lossy() + .to_string(), + port.to_string(), + host.to_string(), + ] + }; + Ok(DebugAdapterBinary { command: Some( delegate @@ -118,14 +139,7 @@ impl JsDebugAdapter { .to_string_lossy() .into_owned(), ), - arguments: vec![ - adapter_path - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - port.to_string(), - host.to_string(), - ], + arguments, cwd: Some(delegate.worktree_root_path().to_path_buf()), envs: HashMap::default(), connection: Some(adapters::TcpArguments { @@ -464,6 +478,7 @@ impl DebugAdapter for JsDebugAdapter { delegate: &Arc, config: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, cx: &mut AsyncApp, ) -> Result { if self.checked.set(()).is_ok() { @@ -481,7 +496,7 @@ impl DebugAdapter for JsDebugAdapter { } } - self.get_installed_binary(delegate, &config, user_installed_path, cx) + self.get_installed_binary(delegate, &config, user_installed_path, user_args, cx) .await } diff --git a/crates/dap_adapters/src/php.rs b/crates/dap_adapters/src/php.rs index 047c744dd9d9d57ad481d818c3ba15ba6b6202a2..7d7dee00c900dcfa44fc4bf99e164d0f2454c817 100644 --- a/crates/dap_adapters/src/php.rs +++ b/crates/dap_adapters/src/php.rs @@ -52,6 +52,7 @@ impl PhpDebugAdapter { delegate: &Arc, task_definition: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, _: &mut AsyncApp, ) -> Result { let adapter_path = if let Some(user_installed_path) = user_installed_path { @@ -77,6 +78,25 @@ impl PhpDebugAdapter { .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into()); } + let arguments = if let Some(mut args) = user_args { + args.insert( + 0, + adapter_path + .join(Self::ADAPTER_PATH) + .to_string_lossy() + .to_string(), + ); + args + } else { + vec![ + adapter_path + .join(Self::ADAPTER_PATH) + .to_string_lossy() + .to_string(), + format!("--server={}", port), + ] + }; + Ok(DebugAdapterBinary { command: Some( delegate @@ -86,13 +106,7 @@ impl PhpDebugAdapter { .to_string_lossy() .into_owned(), ), - arguments: vec![ - adapter_path - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - format!("--server={}", port), - ], + arguments, connection: Some(TcpArguments { port, host, @@ -326,6 +340,7 @@ impl DebugAdapter for PhpDebugAdapter { delegate: &Arc, task_definition: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, cx: &mut AsyncApp, ) -> Result { if self.checked.set(()).is_ok() { @@ -341,7 +356,13 @@ impl DebugAdapter for PhpDebugAdapter { } } - self.get_installed_binary(delegate, &task_definition, user_installed_path, cx) - .await + self.get_installed_binary( + delegate, + &task_definition, + user_installed_path, + user_args, + cx, + ) + .await } } diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index 3a8841bb43e455a0ff36fb96c77303ffece70f74..43d1246d0c8ff1e2580d50b37f02020dc6804c61 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -32,29 +32,23 @@ impl PythonDebugAdapter { host: &Ipv4Addr, port: u16, user_installed_path: Option<&Path>, + user_args: Option>, installed_in_venv: bool, ) -> Result> { - if let Some(user_installed_path) = user_installed_path { + let mut args = if let Some(user_installed_path) = user_installed_path { log::debug!( "Using user-installed debugpy adapter from: {}", user_installed_path.display() ); - Ok(vec![ + vec![ user_installed_path .join(Self::ADAPTER_PATH) .to_string_lossy() .to_string(), - format!("--host={}", host), - format!("--port={}", port), - ]) + ] } else if installed_in_venv { log::debug!("Using venv-installed debugpy"); - Ok(vec![ - "-m".to_string(), - "debugpy.adapter".to_string(), - format!("--host={}", host), - format!("--port={}", port), - ]) + vec!["-m".to_string(), "debugpy.adapter".to_string()] } else { let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()); let file_name_prefix = format!("{}_", Self::ADAPTER_NAME); @@ -70,15 +64,20 @@ impl PythonDebugAdapter { "Using GitHub-downloaded debugpy adapter from: {}", debugpy_dir.display() ); - Ok(vec![ + vec![ debugpy_dir .join(Self::ADAPTER_PATH) .to_string_lossy() .to_string(), - format!("--host={}", host), - format!("--port={}", port), - ]) - } + ] + }; + + args.extend(if let Some(args) = user_args { + args + } else { + vec![format!("--host={}", host), format!("--port={}", port)] + }); + Ok(args) } async fn request_args( @@ -151,6 +150,7 @@ impl PythonDebugAdapter { delegate: &Arc, config: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, toolchain: Option, installed_in_venv: bool, ) -> Result { @@ -182,6 +182,7 @@ impl PythonDebugAdapter { &host, port, user_installed_path.as_deref(), + user_args, installed_in_venv, ) .await?; @@ -595,6 +596,7 @@ impl DebugAdapter for PythonDebugAdapter { delegate: &Arc, config: &DebugTaskDefinition, user_installed_path: Option, + user_args: Option>, cx: &mut AsyncApp, ) -> Result { if let Some(local_path) = &user_installed_path { @@ -603,7 +605,14 @@ impl DebugAdapter for PythonDebugAdapter { local_path.display() ); return self - .get_installed_binary(delegate, &config, Some(local_path.clone()), None, false) + .get_installed_binary( + delegate, + &config, + Some(local_path.clone()), + user_args, + None, + false, + ) .await; } @@ -630,6 +639,7 @@ impl DebugAdapter for PythonDebugAdapter { delegate, &config, None, + user_args, Some(toolchain.clone()), true, ) @@ -647,7 +657,7 @@ impl DebugAdapter for PythonDebugAdapter { } } - self.get_installed_binary(delegate, &config, None, toolchain, false) + self.get_installed_binary(delegate, &config, None, user_args, toolchain, false) .await } } @@ -682,15 +692,21 @@ mod tests { // Case 1: User-defined debugpy path (highest precedence) let user_path = PathBuf::from("/custom/path/to/debugpy"); - let user_args = - PythonDebugAdapter::generate_debugpy_arguments(&host, port, Some(&user_path), false) - .await - .unwrap(); + let user_args = PythonDebugAdapter::generate_debugpy_arguments( + &host, + port, + Some(&user_path), + None, + false, + ) + .await + .unwrap(); // Case 2: Venv-installed debugpy (uses -m debugpy.adapter) - let venv_args = PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, true) - .await - .unwrap(); + let venv_args = + PythonDebugAdapter::generate_debugpy_arguments(&host, port, None, None, true) + .await + .unwrap(); assert!(user_args[0].ends_with("src/debugpy/adapter")); assert_eq!(user_args[1], "--host=127.0.0.1"); @@ -701,6 +717,33 @@ mod tests { assert_eq!(venv_args[2], "--host=127.0.0.1"); assert_eq!(venv_args[3], "--port=5678"); + // The same cases, with arguments overridden by the user + let user_args = PythonDebugAdapter::generate_debugpy_arguments( + &host, + port, + Some(&user_path), + Some(vec!["foo".into()]), + false, + ) + .await + .unwrap(); + let venv_args = PythonDebugAdapter::generate_debugpy_arguments( + &host, + port, + None, + Some(vec!["foo".into()]), + true, + ) + .await + .unwrap(); + + assert!(user_args[0].ends_with("src/debugpy/adapter")); + assert_eq!(user_args[1], "foo"); + + assert_eq!(venv_args[0], "-m"); + assert_eq!(venv_args[1], "debugpy.adapter"); + assert_eq!(venv_args[2], "foo"); + // Note: Case 3 (GitHub-downloaded debugpy) is not tested since this requires mocking the Github API. } } diff --git a/crates/dap_adapters/src/ruby.rs b/crates/dap_adapters/src/ruby.rs index 4e24822f00d32045ad917ad0c21ca34291ef4127..28f1fb1f5ff155329a0629889cfb7d197dd6ce68 100644 --- a/crates/dap_adapters/src/ruby.rs +++ b/crates/dap_adapters/src/ruby.rs @@ -119,6 +119,7 @@ impl DebugAdapter for RubyDebugAdapter { delegate: &Arc, definition: &DebugTaskDefinition, _user_installed_path: Option, + _user_args: Option>, _cx: &mut AsyncApp, ) -> Result { let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref()); diff --git a/crates/debug_adapter_extension/src/extension_dap_adapter.rs b/crates/debug_adapter_extension/src/extension_dap_adapter.rs index 26b9f2e8ad7c7ded2558b0317fe32fae6bfa1f40..b656bed9bc2ec972528c4b4c237e8ae0fceedc5a 100644 --- a/crates/debug_adapter_extension/src/extension_dap_adapter.rs +++ b/crates/debug_adapter_extension/src/extension_dap_adapter.rs @@ -88,6 +88,8 @@ impl DebugAdapter for ExtensionDapAdapter { delegate: &Arc, config: &DebugTaskDefinition, user_installed_path: Option, + // TODO support user args in the extension API + _user_args: Option>, _cx: &mut AsyncApp, ) -> Result { self.extension diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index b54c4c1e45e0e30507199f3a4a0328955e7fab2d..28cfbe4e4d69ae67d99192cf0b99cfbca3f7ee31 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -40,7 +40,7 @@ use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, }; -use settings::{Settings, WorktreeId}; +use settings::{Settings, SettingsLocation, WorktreeId}; use std::{ borrow::Borrow, collections::BTreeMap, @@ -190,17 +190,23 @@ impl DapStore { return Task::ready(Err(anyhow!("Failed to find a debug adapter"))); }; - let user_installed_path = ProjectSettings::get_global(cx) + let settings_location = SettingsLocation { + worktree_id: worktree.read(cx).id(), + path: Path::new(""), + }; + let dap_settings = ProjectSettings::get(Some(settings_location), cx) .dap - .get(&adapter.name()) - .and_then(|s| s.binary.as_ref().map(PathBuf::from)); + .get(&adapter.name()); + let user_installed_path = + dap_settings.and_then(|s| s.binary.as_ref().map(PathBuf::from)); + let user_args = dap_settings.map(|s| s.args.clone()); let delegate = self.delegate(&worktree, console, cx); let cwd: Arc = worktree.read(cx).abs_path().as_ref().into(); cx.spawn(async move |this, cx| { let mut binary = adapter - .get_binary(&delegate, &definition, user_installed_path, cx) + .get_binary(&delegate, &definition, user_installed_path, user_args, cx) .await?; let env = this diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index e1bf3a46a6567653f43ae7f6dc63712f23e62c8b..3f584f969783ca5ac107f592a02c824de5147539 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -82,6 +82,8 @@ pub struct ProjectSettings { #[serde(rename_all = "snake_case")] pub struct DapSettings { pub binary: Option, + #[serde(default)] + pub args: Vec, } #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]