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

@ -5,7 +5,7 @@ use std::net::Ipv4Addr;
use std::path::PathBuf;
use util::ResultExt;
use crate::{TaskTemplate, TaskTemplates, TaskType};
use crate::{task_template::DebugArgs, TaskTemplate, TaskTemplates, TaskType};
impl Default for DebugConnectionType {
fn default() -> Self {
@ -102,6 +102,10 @@ pub struct DebugAdapterConfig {
/// spawning a new process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process.
pub tcp_connection: Option<TCPHost>,
/// What Locator to use to configure the debug task
pub locator: Option<String>,
/// Args to pass to a debug adapter (only used in locator right now)
pub args: Vec<String>,
}
impl From<DebugTaskDefinition> for DebugAdapterConfig {
@ -112,6 +116,8 @@ impl From<DebugTaskDefinition> for DebugAdapterConfig {
request: DebugRequestDisposition::UserConfigured(def.request),
initialize_args: def.initialize_args,
tcp_connection: def.tcp_connection,
locator: def.locator,
args: def.args,
}
}
}
@ -130,6 +136,8 @@ impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
request,
initialize_args: def.initialize_args,
tcp_connection: def.tcp_connection,
locator: def.locator,
args: def.args,
})
}
}
@ -137,18 +145,30 @@ impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
impl DebugTaskDefinition {
/// Translate from debug definition to a task template
pub fn to_zed_format(self) -> anyhow::Result<TaskTemplate> {
let command = "".to_string();
let cwd = if let DebugRequestType::Launch(ref launch) = self.request {
launch
.cwd
.as_ref()
.map(|path| path.to_string_lossy().into_owned())
} else {
None
let (command, cwd, request) = match self.request {
DebugRequestType::Launch(launch_config) => (
launch_config.program,
launch_config
.cwd
.map(|cwd| cwd.to_string_lossy().to_string()),
crate::task_template::DebugArgsRequest::Launch,
),
DebugRequestType::Attach(attach_config) => (
"".to_owned(),
None,
crate::task_template::DebugArgsRequest::Attach(attach_config),
),
};
let task_type = TaskType::Debug(DebugArgs {
adapter: self.adapter,
request,
initialize_args: self.initialize_args,
locator: self.locator,
tcp_connection: self.tcp_connection,
});
let label = self.label.clone();
let task_type = TaskType::Debug(self);
Ok(TaskTemplate {
label,
@ -189,6 +209,12 @@ pub struct DebugTaskDefinition {
/// spawning a new process. This is useful for connecting to a debug adapter
/// that is already running or is started by another process.
pub tcp_connection: Option<TCPHost>,
/// Locator to use
/// -- cargo
pub locator: Option<String>,
/// Args to pass to a debug adapter (only used in locator right now)
#[serde(skip)]
pub args: Vec<String>,
}
/// A group of Debug Tasks defined in a JSON file.

View file

@ -19,7 +19,8 @@ pub use debug_format::{
DebugRequestType, DebugTaskDefinition, DebugTaskFile, LaunchConfig, TCPHost,
};
pub use task_template::{
HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates, TaskType,
DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
TaskTemplates, TaskType,
};
pub use vscode_format::VsCodeTaskFile;
pub use zed_actions::RevealTarget;
@ -61,8 +62,6 @@ pub struct SpawnInTerminal {
pub hide: HideStrategy,
/// Which shell to use when spawning the task.
pub shell: Shell,
/// Tells debug tasks which program to debug
pub program: Option<String>,
/// Whether to show the task summary line in the task output (sucess/failure).
pub show_summary: bool,
/// Whether to show the command line in the task output.
@ -104,24 +103,50 @@ impl ResolvedTask {
}
/// Get the configuration for the debug adapter that should be used for this task.
pub fn resolved_debug_adapter_config(&self) -> Option<DebugTaskDefinition> {
pub fn resolved_debug_adapter_config(&self) -> Option<DebugAdapterConfig> {
match self.original_task.task_type.clone() {
TaskType::Script => None,
TaskType::Debug(mut adapter_config) => {
if let Some(resolved) = &self.resolved {
adapter_config.label = resolved.label.clone();
if let DebugRequestType::Launch(ref mut launch) = adapter_config.request {
if let Some(program) = resolved.program.clone() {
launch.program = program;
}
if let Some(cwd) = resolved.cwd.clone() {
launch.cwd = Some(cwd);
}
}
}
TaskType::Debug(debug_args) if self.resolved.is_some() => {
let resolved = self
.resolved
.as_ref()
.expect("We just checked if this was some");
Some(adapter_config)
let args = resolved
.args
.iter()
.cloned()
.map(|arg| {
if arg.starts_with("$") {
arg.strip_prefix("$")
.and_then(|arg| resolved.env.get(arg).map(ToOwned::to_owned))
.unwrap_or_else(|| arg)
} else {
arg
}
})
.collect();
Some(DebugAdapterConfig {
label: resolved.label.clone(),
adapter: debug_args.adapter.clone(),
request: DebugRequestDisposition::UserConfigured(match debug_args.request {
crate::task_template::DebugArgsRequest::Launch => {
DebugRequestType::Launch(LaunchConfig {
program: resolved.command.clone(),
cwd: resolved.cwd.clone(),
})
}
crate::task_template::DebugArgsRequest::Attach(attach_config) => {
DebugRequestType::Attach(attach_config)
}
}),
initialize_args: debug_args.initialize_args,
tcp_connection: debug_args.tcp_connection,
args,
locator: debug_args.locator.clone(),
})
}
_ => None,
}
}

View file

@ -9,8 +9,8 @@ use sha2::{Digest, Sha256};
use util::{truncate_and_remove_front, ResultExt};
use crate::{
DebugRequestType, DebugTaskDefinition, ResolvedTask, RevealTarget, Shell, SpawnInTerminal,
TaskContext, TaskId, VariableName, ZED_VARIABLE_NAME_PREFIX,
AttachConfig, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TCPHost, TaskContext, TaskId,
VariableName, ZED_VARIABLE_NAME_PREFIX,
};
/// A template definition of a Zed task to run.
@ -75,62 +75,39 @@ pub struct TaskTemplate {
pub show_command: bool,
}
#[derive(Deserialize, Eq, PartialEq, Clone, Debug)]
/// Use to represent debug request type
pub enum DebugArgsRequest {
/// launch (program, cwd) are stored in TaskTemplate as (command, cwd)
Launch,
/// Attach
Attach(AttachConfig),
}
#[derive(Deserialize, Eq, PartialEq, Clone, Debug)]
/// This represents the arguments for the debug task.
pub struct DebugArgs {
/// The launch type
pub request: DebugArgsRequest,
/// Adapter choice
pub adapter: String,
/// TCP connection to make with debug adapter
pub tcp_connection: Option<TCPHost>,
/// Args to send to debug adapter
pub initialize_args: Option<serde_json::value::Value>,
/// the locator to use
pub locator: Option<String>,
}
/// Represents the type of task that is being ran
#[derive(Default, Deserialize, Serialize, Eq, PartialEq, JsonSchema, Clone, Debug)]
#[serde(rename_all = "snake_case", tag = "type")]
#[derive(Default, Eq, PartialEq, Clone, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum TaskType {
/// Act like a typically task that runs commands
#[default]
Script,
/// This task starts the debugger for a language
Debug(DebugTaskDefinition),
}
#[cfg(test)]
mod deserialization_tests {
use crate::LaunchConfig;
use super::*;
use serde_json::json;
#[test]
fn deserialize_task_type_script() {
let json = json!({"type": "script"});
let task_type: TaskType =
serde_json::from_value(json).expect("Failed to deserialize TaskType::Script");
assert_eq!(task_type, TaskType::Script);
}
#[test]
fn deserialize_task_type_debug() {
let adapter_config = DebugTaskDefinition {
label: "test config".into(),
adapter: "Debugpy".into(),
request: crate::DebugRequestType::Launch(LaunchConfig {
program: "main".to_string(),
cwd: None,
}),
initialize_args: None,
tcp_connection: None,
};
let json = json!({
"label": "test config",
"type": "debug",
"adapter": "Debugpy",
"program": "main",
"supports_attach": false,
});
let task_type: TaskType =
serde_json::from_value(json).expect("Failed to deserialize TaskType::Debug");
if let TaskType::Debug(config) = task_type {
assert_eq!(config, adapter_config);
} else {
panic!("Expected TaskType::Debug");
}
}
Debug(DebugArgs),
}
#[derive(Clone, Debug, PartialEq, Eq)]
@ -270,22 +247,6 @@ impl TaskTemplate {
&mut substituted_variables,
)?;
let program = match &self.task_type {
TaskType::Script => None,
TaskType::Debug(adapter_config) => {
if let DebugRequestType::Launch(ref launch) = &adapter_config.request {
Some(substitute_all_template_variables_in_str(
&launch.program,
&task_variables,
&variable_names,
&mut substituted_variables,
)?)
} else {
None
}
}
};
let task_hash = to_hex_hash(self)
.context("hashing task template")
.log_err()?;
@ -341,7 +302,6 @@ impl TaskTemplate {
reveal_target: self.reveal_target,
hide: self.hide,
shell: self.shell.clone(),
program,
show_summary: self.show_summary,
show_command: self.show_command,
show_rerun: true,