Set up Rust debugger code runner tasks (#27571)

## Summary 
This PR starts the process of adding debug task locators to Zed's
debugger system. A task locator is a secondary resolution phase that
allows a debug task to run a command before starting a debug session and
then uses the output of the run command to configure itself.

Locators are most applicable when debugging a compiled language but will
be helpful for any language as well.

## Architecture

At a high level, this works by adding a debug task queue to `Workspace`.
Which add's a debug configuration associated with a `TaskId` whenever a
resolved task with a debug config is added to `TaskInventory`'s queue.
Then, when the `SpawnInTerminal` task finishes running, it emits its
task_id and the result of the ran task.

When a ran task exits successfully, `Workspace` tells `Project` to start
a debug session using its stored debug config, then `DapStore` queries
the `LocatorStore` to configure the debug configuration if it has a
valid locator argument.

Release Notes:

- N/A
This commit is contained in:
Anthony Eid 2025-03-29 02:10:40 -04:00 committed by GitHub
parent 141a6c3915
commit 8add90d7cb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 441 additions and 168 deletions

View file

@ -12,7 +12,7 @@ use rpc::{
AnyProtoClient, TypedEnvelope,
};
use std::{hash::Hash, ops::Range, path::Path, sync::Arc};
use text::{Point, PointUtf16};
use text::PointUtf16;
use crate::{buffer_store::BufferStore, worktree_store::WorktreeStore, Project, ProjectPath};
@ -494,7 +494,7 @@ impl BreakpointStore {
this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?;
for bp in bps {
let position = snapshot.anchor_after(Point::new(bp.row, 0));
let position = snapshot.anchor_after(PointUtf16::new(bp.row, 0));
breakpoints_for_file.breakpoints.push((
position,
Breakpoint {

View file

@ -1,11 +1,6 @@
use super::{
breakpoint_store::BreakpointStore,
// Will need to uncomment this once we implement rpc message handler again
// dap_command::{
// ContinueCommand, DapCommand, DisconnectCommand, NextCommand, PauseCommand, RestartCommand,
// RestartStackFrameCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand,
// TerminateCommand, TerminateThreadsCommand, VariablesCommand,
// },
locator_store::LocatorStore,
session::{self, Session},
};
use crate::{debugger, worktree_store::WorktreeStore, ProjectEnvironment};
@ -87,6 +82,7 @@ pub struct LocalDapStore {
language_registry: Arc<LanguageRegistry>,
debug_adapters: Arc<DapRegistry>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
locator_store: Arc<LocatorStore>,
start_debugging_tx: futures::channel::mpsc::UnboundedSender<(SessionId, Message)>,
_start_debugging_task: Task<()>,
}
@ -179,6 +175,7 @@ impl DapStore {
debug_adapters,
start_debugging_tx,
_start_debugging_task,
locator_store: Arc::from(LocatorStore::new()),
next_session_id: Default::default(),
}),
downstream_client: None,
@ -324,7 +321,7 @@ impl DapStore {
pub fn new_session(
&mut self,
config: DebugAdapterConfig,
mut config: DebugAdapterConfig,
worktree: &Entity<Worktree>,
parent_session: Option<Entity<Session>>,
cx: &mut Context<Self>,
@ -354,22 +351,39 @@ impl DapStore {
}
let (initialized_tx, initialized_rx) = oneshot::channel();
let locator_store = local_store.locator_store.clone();
let debug_adapters = local_store.debug_adapters.clone();
let start_client_task = Session::local(
self.breakpoint_store.clone(),
session_id,
parent_session,
delegate,
config,
local_store.start_debugging_tx.clone(),
initialized_tx,
local_store.debug_adapters.clone(),
cx,
);
let start_debugging_tx = local_store.start_debugging_tx.clone();
let task = cx.spawn(async move |this, cx| {
if config.locator.is_some() {
locator_store.resolve_debug_config(&mut config).await?;
}
let start_client_task = this.update(cx, |this, cx| {
Session::local(
this.breakpoint_store.clone(),
session_id,
parent_session,
delegate,
config,
start_debugging_tx.clone(),
initialized_tx,
debug_adapters,
cx,
)
})?;
this.update(cx, |_, cx| {
create_new_session(session_id, initialized_rx, start_client_task, cx)
})?
.await
});
let task = create_new_session(session_id, initialized_rx, start_client_task, cx);
(session_id, task)
}
#[cfg(any(test, feature = "test-support"))]
pub fn new_fake_session(
&mut self,
@ -456,7 +470,10 @@ impl DapStore {
request: DebugRequestDisposition::ReverseRequest(args),
initialize_args: config.initialize_args.clone(),
tcp_connection: config.tcp_connection.clone(),
locator: None,
args: Default::default(),
};
#[cfg(any(test, feature = "test-support"))]
let new_session_task = {
let caps = parent_session.read(cx).capabilities.clone();

View file

@ -0,0 +1,39 @@
use anyhow::{anyhow, Result};
use cargo::CargoLocator;
use collections::HashMap;
use dap::DebugAdapterConfig;
use gpui::SharedString;
use locators::DapLocator;
mod cargo;
mod locators;
pub(super) struct LocatorStore {
locators: HashMap<SharedString, Box<dyn DapLocator>>,
}
impl LocatorStore {
pub(super) fn new() -> Self {
let locators = HashMap::from_iter([(
SharedString::new("cargo"),
Box::new(CargoLocator {}) as Box<dyn DapLocator>,
)]);
Self { locators }
}
pub(super) async fn resolve_debug_config(
&self,
debug_config: &mut DebugAdapterConfig,
) -> Result<()> {
let Some(ref locator_name) = &debug_config.locator else {
log::debug!("Attempted to resolve debug config without a locator field");
return Ok(());
};
if let Some(locator) = self.locators.get(locator_name as &str) {
locator.run_locator(debug_config).await
} else {
Err(anyhow!("Couldn't find locator {}", locator_name))
}
}
}

View file

@ -0,0 +1,65 @@
use super::DapLocator;
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use dap::DebugAdapterConfig;
use serde_json::Value;
use smol::{
io::AsyncReadExt,
process::{Command, Stdio},
};
pub(super) struct CargoLocator {}
#[async_trait]
impl DapLocator for CargoLocator {
async fn run_locator(&self, debug_config: &mut DebugAdapterConfig) -> Result<()> {
let Some(launch_config) = (match &mut debug_config.request {
task::DebugRequestDisposition::UserConfigured(task::DebugRequestType::Launch(
launch_config,
)) => Some(launch_config),
_ => None,
}) else {
return Err(anyhow!("Couldn't get launch config in locator"));
};
let Some(cwd) = launch_config.cwd.clone() else {
return Err(anyhow!(
"Couldn't get cwd from debug config which is needed for locators"
));
};
let mut child = Command::new("cargo")
.args(&debug_config.args)
.arg("--message-format=json")
.current_dir(cwd)
.stdout(Stdio::piped())
.spawn()?;
let mut output = String::new();
if let Some(mut stdout) = child.stdout.take() {
stdout.read_to_string(&mut output).await?;
}
let status = child.status().await?;
if !status.success() {
return Err(anyhow::anyhow!("Cargo command failed"));
}
let Some(executable) = output
.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| serde_json::from_str(line).ok())
.find_map(|json: Value| {
json.get("executable")
.and_then(Value::as_str)
.map(String::from)
})
else {
return Err(anyhow!("Couldn't get executable in cargo locator"));
};
launch_config.program = executable;
debug_config.args.clear();
Ok(())
}
}

View file

@ -0,0 +1,8 @@
use anyhow::Result;
use async_trait::async_trait;
use dap::DebugAdapterConfig;
#[async_trait]
pub(super) trait DapLocator {
async fn run_locator(&self, debug_config: &mut DebugAdapterConfig) -> Result<()>;
}