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 <kirill@zed.dev>
This commit is contained in:
Piotr Osiewicz 2025-05-28 12:27:12 +02:00 committed by GitHub
parent 15d59fcda9
commit c0a5ace8b8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 118 additions and 3 deletions

View file

@ -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)

View file

@ -1 +1,2 @@
pub(crate) mod cargo;
pub(crate) mod python;

View file

@ -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<DebugScenario> {
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::<Vec<_>>();
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::<Vec<_>>()
} 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<DebugRequest> {
bail!("Python locator should not require DapLocator::run to be ran");
}
}