From c0a5ace8b8d42f310787c9c43b3c65d13468a6d8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 28 May 2025 12:27:12 +0200 Subject: [PATCH] debugger: Add locator for Python tasks (#31533) Closes #ISSUE Release Notes: - debugger: Python tests/main functions can now we debugged from the gutter. --------- Co-authored-by: Kirill Bulatov --- crates/debugger_ui/src/debugger_panel.rs | 1 - crates/debugger_ui/src/session/running.rs | 8 ++ crates/languages/src/python.rs | 7 ++ crates/project/src/debugger/dap_store.rs | 5 +- crates/project/src/debugger/locators.rs | 1 + .../project/src/debugger/locators/python.rs | 99 +++++++++++++++++++ 6 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 crates/project/src/debugger/locators/python.rs diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 1786dc9384..bc22962faa 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -295,7 +295,6 @@ impl DebugPanel { }) })? .await?; - dap_store .update(cx, |dap_store, cx| { dap_store.boot_session(session.clone(), definition, cx) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 8d0d6c617c..331961e089 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -547,6 +547,10 @@ impl RunningState { .for_each(|value| Self::substitute_variables_in_config(value, context)); } serde_json::Value::String(s) => { + // Some built-in zed tasks wrap their arguments in quotes as they might contain spaces. + if s.starts_with("\"$ZED_") && s.ends_with('"') { + *s = s[1..s.len() - 1].to_string(); + } if let Some(substituted) = substitute_variables_in_str(&s, context) { *s = substituted; } @@ -571,6 +575,10 @@ impl RunningState { .for_each(|value| Self::relativlize_paths(None, value, context)); } serde_json::Value::String(s) if key == Some("program") || key == Some("cwd") => { + // Some built-in zed tasks wrap their arguments in quotes as they might contain spaces. + if s.starts_with("\"$ZED_") && s.ends_with('"') { + *s = s[1..s.len() - 1].to_string(); + } resolve_path(s); if let Some(substituted) = substitute_variables_in_str(&s, context) { diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 29b376bd98..ea0e348c10 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -413,6 +413,7 @@ impl ContextProvider for PythonContextProvider { "-c".to_owned(), VariableName::SelectedText.template_value_with_whitespace(), ], + cwd: Some("$ZED_WORKTREE_ROOT".into()), ..TaskTemplate::default() }, // Execute an entire file @@ -420,6 +421,7 @@ impl ContextProvider for PythonContextProvider { label: format!("run '{}'", VariableName::File.template_value()), command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(), args: vec![VariableName::File.template_value_with_whitespace()], + cwd: Some("$ZED_WORKTREE_ROOT".into()), ..TaskTemplate::default() }, // Execute a file as module @@ -430,6 +432,7 @@ impl ContextProvider for PythonContextProvider { "-m".to_owned(), PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(), ], + cwd: Some("$ZED_WORKTREE_ROOT".into()), tags: vec!["python-module-main-method".to_owned()], ..TaskTemplate::default() }, @@ -447,6 +450,7 @@ impl ContextProvider for PythonContextProvider { "unittest".to_owned(), VariableName::File.template_value_with_whitespace(), ], + cwd: Some("$ZED_WORKTREE_ROOT".into()), ..TaskTemplate::default() }, // Run test(s) for a specific target within a file @@ -462,6 +466,7 @@ impl ContextProvider for PythonContextProvider { "python-unittest-class".to_owned(), "python-unittest-method".to_owned(), ], + cwd: Some("$ZED_WORKTREE_ROOT".into()), ..TaskTemplate::default() }, ] @@ -477,6 +482,7 @@ impl ContextProvider for PythonContextProvider { "pytest".to_owned(), VariableName::File.template_value_with_whitespace(), ], + cwd: Some("$ZED_WORKTREE_ROOT".into()), ..TaskTemplate::default() }, // Run test(s) for a specific target within a file @@ -488,6 +494,7 @@ impl ContextProvider for PythonContextProvider { "pytest".to_owned(), PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(), ], + cwd: Some("$ZED_WORKTREE_ROOT".into()), tags: vec![ "python-pytest-class".to_owned(), "python-pytest-method".to_owned(), diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 622630227a..bdcd2c53e3 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -101,7 +101,9 @@ impl DapStore { pub fn init(client: &AnyProtoClient, cx: &mut App) { static ADD_LOCATORS: Once = Once::new(); ADD_LOCATORS.call_once(|| { - DapRegistry::global(cx).add_locator(Arc::new(locators::cargo::CargoLocator {})) + let registry = DapRegistry::global(cx); + registry.add_locator(Arc::new(locators::cargo::CargoLocator {})); + registry.add_locator(Arc::new(locators::python::PythonLocator)); }); client.add_entity_request_handler(Self::handle_run_debug_locator); client.add_entity_request_handler(Self::handle_get_debug_adapter_binary); @@ -412,7 +414,6 @@ impl DapStore { this.get_debug_adapter_binary(definition.clone(), session_id, console, cx) })? .await?; - session .update(cx, |session, cx| { session.boot(binary, worktree, dap_store, cx) diff --git a/crates/project/src/debugger/locators.rs b/crates/project/src/debugger/locators.rs index a0108cf57b..d4a64118d7 100644 --- a/crates/project/src/debugger/locators.rs +++ b/crates/project/src/debugger/locators.rs @@ -1 +1,2 @@ pub(crate) mod cargo; +pub(crate) mod python; diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs new file mode 100644 index 0000000000..d331d0258e --- /dev/null +++ b/crates/project/src/debugger/locators/python.rs @@ -0,0 +1,99 @@ +use std::path::Path; + +use anyhow::{Result, bail}; +use async_trait::async_trait; +use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; +use gpui::SharedString; + +use task::{DebugScenario, SpawnInTerminal, TaskTemplate}; + +pub(crate) struct PythonLocator; + +#[async_trait] +impl DapLocator for PythonLocator { + fn name(&self) -> SharedString { + SharedString::new_static("Python") + } + + /// Determines whether this locator can generate debug target for given task. + fn create_scenario( + &self, + build_config: &TaskTemplate, + resolved_label: &str, + adapter: DebugAdapterName, + ) -> Option { + if adapter.as_ref() != "Debugpy" { + return None; + } + let valid_program = build_config.command.starts_with("$ZED_") + || Path::new(&build_config.command) + .file_name() + .map_or(false, |name| { + name.to_str().is_some_and(|path| path.starts_with("python")) + }); + if !valid_program || build_config.args.iter().any(|arg| arg == "-c") { + // We cannot debug selections. + return None; + } + let module_specifier_position = build_config + .args + .iter() + .position(|arg| arg == "-m") + .map(|position| position + 1); + // Skip the -m and module name, get all that's after. + let mut rest_of_the_args = module_specifier_position + .and_then(|position| build_config.args.get(position..)) + .into_iter() + .flatten() + .fuse(); + let mod_name = rest_of_the_args.next(); + let args = rest_of_the_args.collect::>(); + + let program_position = mod_name + .is_none() + .then(|| { + build_config + .args + .iter() + .position(|arg| *arg == "\"$ZED_FILE\"") + }) + .flatten(); + let args = if let Some(position) = program_position { + args.into_iter().skip(position).collect::>() + } else { + args + }; + if program_position.is_none() && mod_name.is_none() { + return None; + } + let mut config = serde_json::json!({ + "request": "launch", + "python": build_config.command, + "args": args, + "cwd": build_config.cwd.clone() + }); + if let Some(config_obj) = config.as_object_mut() { + if let Some(module) = mod_name { + config_obj.insert("module".to_string(), module.clone().into()); + } + if let Some(program) = program_position { + config_obj.insert( + "program".to_string(), + build_config.args[program].clone().into(), + ); + } + } + + Some(DebugScenario { + adapter: adapter.0, + label: resolved_label.to_string().into(), + build: None, + config, + tcp_connection: None, + }) + } + + async fn run(&self, _: SpawnInTerminal) -> Result { + bail!("Python locator should not require DapLocator::run to be ran"); + } +}