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 a269c099cc..8e1c84083f 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 3158996681..5d14cc8747 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 e889588359..17b7a65911 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 afd733b56a..bc3f500745 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 e59fb101ff..d5d78186ac 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 047c744dd9..7d7dee00c9 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 3a8841bb43..43d1246d0c 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 4e24822f00..28f1fb1f5f 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 26b9f2e8ad..b656bed9bc 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 b54c4c1e45..28cfbe4e4d 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 e1bf3a46a6..3f584f9697 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)]