Set up Rust debugger code runner tasks (#27571)

Anthony Eid created

## 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

Change summary

crates/dap_adapters/src/go.rs                         |  5 
crates/dap_adapters/src/javascript.rs                 |  6 
crates/dap_adapters/src/php.rs                        |  5 
crates/dap_adapters/src/python.rs                     |  5 
crates/debugger_ui/src/session/inert.rs               |  4 
crates/debugger_ui/src/tests/attach_modal.rs          |  2 
crates/editor/src/editor.rs                           | 54 +++++-
crates/languages/src/rust.rs                          | 49 ++++++
crates/project/src/debugger.rs                        |  1 
crates/project/src/debugger/breakpoint_store.rs       |  4 
crates/project/src/debugger/dap_store.rs              | 55 ++++--
crates/project/src/debugger/locator_store.rs          | 39 +++++
crates/project/src/debugger/locator_store/cargo.rs    | 65 ++++++++
crates/project/src/debugger/locator_store/locators.rs |  8 +
crates/project/src/project.rs                         | 10 +
crates/task/src/debug_format.rs                       | 48 +++++-
crates/task/src/lib.rs                                | 59 +++++--
crates/task/src/task_template.rs                      | 96 +++---------
crates/tasks_ui/src/modal.rs                          | 54 ++++--
crates/terminal/src/terminal.rs                       |  6 
crates/terminal_view/src/terminal_view.rs             |  9 +
crates/vim/src/command.rs                             |  1 
crates/workspace/src/tasks.rs                         |  8 +
crates/workspace/src/workspace.rs                     | 14 +
24 files changed, 440 insertions(+), 167 deletions(-)

Detailed changes

crates/dap_adapters/src/go.rs 🔗

@@ -1,4 +1,3 @@
-use anyhow::bail;
 use gpui::AsyncApp;
 use std::{ffi::OsStr, path::PathBuf};
 use task::DebugTaskDefinition;
@@ -63,9 +62,7 @@ impl DebugAdapter for GoDebugAdapter {
             .and_then(|p| p.to_str().map(|p| p.to_string()))
             .ok_or(anyhow!("Dlv not found in path"))?;
 
-        let Some(tcp_connection) = config.tcp_connection.clone() else {
-            bail!("Go Debug Adapter expects tcp connection arguments to be provided");
-        };
+        let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
         Ok(DebugAdapterBinary {

crates/dap_adapters/src/javascript.rs 🔗

@@ -78,11 +78,7 @@ impl DebugAdapter for JsDebugAdapter {
             .ok_or_else(|| anyhow!("Couldn't find JavaScript dap directory"))?
         };
 
-        let Some(tcp_connection) = config.tcp_connection.clone() else {
-            anyhow::bail!(
-                "Javascript Debug Adapter expects tcp connection arguments to be provided"
-            );
-        };
+        let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
         Ok(DebugAdapterBinary {

crates/dap_adapters/src/php.rs 🔗

@@ -1,5 +1,4 @@
 use adapters::latest_github_release;
-use anyhow::bail;
 use dap::adapters::TcpArguments;
 use gpui::AsyncApp;
 use std::path::PathBuf;
@@ -69,9 +68,7 @@ impl DebugAdapter for PhpDebugAdapter {
             .ok_or_else(|| anyhow!("Couldn't find PHP dap directory"))?
         };
 
-        let Some(tcp_connection) = config.tcp_connection.clone() else {
-            bail!("PHP Debug Adapter expects tcp connection arguments to be provided");
-        };
+        let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
         Ok(DebugAdapterBinary {

crates/dap_adapters/src/python.rs 🔗

@@ -1,5 +1,4 @@
 use crate::*;
-use anyhow::bail;
 use dap::DebugRequestType;
 use gpui::AsyncApp;
 use std::{ffi::OsStr, path::PathBuf};
@@ -70,9 +69,7 @@ impl DebugAdapter for PythonDebugAdapter {
         cx: &mut AsyncApp,
     ) -> Result<DebugAdapterBinary> {
         const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
-        let Some(tcp_connection) = config.tcp_connection.clone() else {
-            bail!("Python Debug Adapter expects tcp connection arguments to be provided");
-        };
+        let tcp_connection = config.tcp_connection.clone().unwrap_or_default();
         let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
 
         let debugpy_dir = if let Some(user_installed_path) = user_installed_path {

crates/debugger_ui/src/session/inert.rs 🔗

@@ -159,6 +159,8 @@ impl Render for InertState {
                             }),
                             tcp_connection: Some(TCPHost::default()),
                             initialize_args: None,
+                            args: Default::default(),
+                            locator: None,
                         },
                     });
                 } else {
@@ -319,6 +321,8 @@ impl InertState {
             adapter: kind,
             request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
             initialize_args: None,
+            args: Default::default(),
+            locator: None,
             tcp_connection: Some(TCPHost::default()),
         };
 

crates/debugger_ui/src/tests/attach_modal.rs 🔗

@@ -89,6 +89,8 @@ async fn test_show_attach_modal_and_select_process(
                         label: "attach example".into(),
                         initialize_args: None,
                         tcp_connection: Some(TCPHost::default()),
+                        locator: None,
+                        args: Default::default(),
                     },
                     vec![
                         Candidate {

crates/editor/src/editor.rs 🔗

@@ -4850,6 +4850,7 @@ impl Editor {
             } else {
                 return None;
             };
+
         let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
         let action = actions_menu.actions.get(action_ix)?;
         let title = action.label();
@@ -4858,17 +4859,48 @@ impl Editor {
 
         match action {
             CodeActionsItem::Task(task_source_kind, resolved_task) => {
-                workspace.update(cx, |workspace, cx| {
-                    workspace::tasks::schedule_resolved_task(
-                        workspace,
-                        task_source_kind,
-                        resolved_task,
-                        false,
-                        cx,
-                    );
+                match resolved_task.task_type() {
+                    task::TaskType::Script => workspace.update(cx, |workspace, cx| {
+                        workspace::tasks::schedule_resolved_task(
+                            workspace,
+                            task_source_kind,
+                            resolved_task,
+                            false,
+                            cx,
+                        );
 
-                    Some(Task::ready(Ok(())))
-                })
+                        Some(Task::ready(Ok(())))
+                    }),
+                    task::TaskType::Debug(debug_args) => {
+                        if debug_args.locator.is_some() {
+                            workspace.update(cx, |workspace, cx| {
+                                workspace::tasks::schedule_resolved_task(
+                                    workspace,
+                                    task_source_kind,
+                                    resolved_task,
+                                    false,
+                                    cx,
+                                );
+                            });
+
+                            return Some(Task::ready(Ok(())));
+                        }
+
+                        if let Some(project) = self.project.as_ref() {
+                            project
+                                .update(cx, |project, cx| {
+                                    project.start_debug_session(
+                                        resolved_task.resolved_debug_adapter_config().unwrap(),
+                                        cx,
+                                    )
+                                })
+                                .detach_and_log_err(cx);
+                            Some(Task::ready(Ok(())))
+                        } else {
+                            Some(Task::ready(Ok(())))
+                        }
+                    }
+                }
             }
             CodeActionsItem::CodeAction {
                 excerpt_id,
@@ -8600,7 +8632,7 @@ impl Editor {
         let line_len = snapshot.buffer_snapshot.line_len(MultiBufferRow(row));
         let anchor_end = snapshot
             .buffer_snapshot
-            .anchor_before(Point::new(row, line_len));
+            .anchor_after(Point::new(row, line_len));
 
         let bp = self
             .breakpoint_store

crates/languages/src/rust.rs 🔗

@@ -17,7 +17,7 @@ use std::{
     path::{Path, PathBuf},
     sync::{Arc, LazyLock},
 };
-use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
+use task::{TaskTemplate, TaskTemplates, TaskType, TaskVariables, VariableName};
 use util::{fs::remove_matching, maybe, ResultExt};
 
 use crate::language_settings::language_settings;
@@ -574,11 +574,16 @@ impl ContextProvider for RustContextProvider {
             .variables
             .get(CUSTOM_TARGET_DIR)
             .cloned();
-        let run_task_args = if let Some(package_to_run) = package_to_run {
+        let run_task_args = if let Some(package_to_run) = package_to_run.clone() {
             vec!["run".into(), "-p".into(), package_to_run]
         } else {
             vec!["run".into()]
         };
+        let debug_task_args = if let Some(package_to_run) = package_to_run {
+            vec!["build".into(), "-p".into(), package_to_run]
+        } else {
+            vec!["build".into()]
+        };
         let mut task_templates = vec![
             TaskTemplate {
                 label: format!(
@@ -620,6 +625,31 @@ impl ContextProvider for RustContextProvider {
                 cwd: Some("$ZED_DIRNAME".to_owned()),
                 ..TaskTemplate::default()
             },
+            TaskTemplate {
+                label: format!(
+                    "Debug Test '{}' (package: {})",
+                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
+                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
+                ),
+                task_type: TaskType::Debug(task::DebugArgs {
+                    adapter: "LLDB".to_owned(),
+                    request: task::DebugArgsRequest::Launch,
+                    locator: Some("cargo".into()),
+                    tcp_connection: None,
+                    initialize_args: None,
+                }),
+                command: "cargo".into(),
+                args: vec![
+                    "test".into(),
+                    "-p".into(),
+                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
+                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
+                    "--no-run".into(),
+                ],
+                tags: vec!["rust-test".to_owned()],
+                cwd: Some("$ZED_DIRNAME".to_owned()),
+                ..TaskTemplate::default()
+            },
             TaskTemplate {
                 label: format!(
                     "Doc test '{}' (package: {})",
@@ -697,6 +727,21 @@ impl ContextProvider for RustContextProvider {
                 cwd: Some("$ZED_DIRNAME".to_owned()),
                 ..TaskTemplate::default()
             },
+            TaskTemplate {
+                label: "Debug".into(),
+                cwd: Some("$ZED_DIRNAME".to_owned()),
+                command: "cargo".into(),
+                task_type: TaskType::Debug(task::DebugArgs {
+                    request: task::DebugArgsRequest::Launch,
+                    adapter: "LLDB".to_owned(),
+                    initialize_args: None,
+                    locator: Some("cargo".into()),
+                    tcp_connection: None,
+                }),
+                args: debug_task_args,
+                tags: vec!["rust-main".to_owned()],
+                ..TaskTemplate::default()
+            },
             TaskTemplate {
                 label: "Clean".into(),
                 command: "cargo".into(),

crates/project/src/debugger/breakpoint_store.rs 🔗

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

crates/project/src/debugger/dap_store.rs 🔗

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

crates/project/src/debugger/locator_store.rs 🔗

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

crates/project/src/debugger/locator_store/cargo.rs 🔗

@@ -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(())
+    }
+}

crates/project/src/debugger/locator_store/locators.rs 🔗

@@ -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<()>;
+}

crates/project/src/project.rs 🔗

@@ -165,6 +165,7 @@ pub struct Project {
     languages: Arc<LanguageRegistry>,
     debug_adapters: Arc<DapRegistry>,
     dap_store: Entity<DapStore>,
+
     breakpoint_store: Entity<BreakpointStore>,
     client: Arc<client::Client>,
     join_project_response_message_id: u32,
@@ -952,6 +953,7 @@ impl Project {
                 ssh_client: None,
                 breakpoint_store,
                 dap_store,
+
                 buffers_needing_diff: Default::default(),
                 git_diff_debouncer: DebouncedDelay::new(),
                 terminals: Terminals {
@@ -1450,6 +1452,12 @@ impl Project {
         }
     }
 
+    pub fn queue_debug_session(&mut self, config: DebugAdapterConfig, cx: &mut Context<Self>) {
+        if config.locator.is_none() {
+            self.start_debug_session(config, cx).detach_and_log_err(cx);
+        }
+    }
+
     pub fn start_debug_session(
         &mut self,
         config: DebugAdapterConfig,
@@ -1490,6 +1498,8 @@ impl Project {
             request: DebugRequestDisposition::UserConfigured(request),
             initialize_args: None,
             tcp_connection: None,
+            locator: None,
+            args: Default::default(),
         };
         let caps = caps.unwrap_or(Capabilities {
             supports_step_back: Some(false),

crates/task/src/debug_format.rs 🔗

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

crates/task/src/lib.rs 🔗

@@ -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;
+            TaskType::Debug(debug_args) if self.resolved.is_some() => {
+                let resolved = self
+                    .resolved
+                    .as_ref()
+                    .expect("We just checked if this was some");
+
+                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
                         }
-                        if let Some(cwd) = resolved.cwd.clone() {
-                            launch.cwd = Some(cwd);
+                    })
+                    .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(),
+                            })
                         }
-                    }
-                }
-
-                Some(adapter_config)
+                        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,
         }
     }
 

crates/task/src/task_template.rs 🔗

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

crates/tasks_ui/src/modal.rs 🔗

@@ -10,7 +10,8 @@ use gpui::{
 use picker::{highlighted_match_with_paths::HighlightedMatch, Picker, PickerDelegate};
 use project::{task_store::TaskStore, TaskSourceKind};
 use task::{
-    DebugRequestType, ResolvedTask, RevealTarget, TaskContext, TaskModal, TaskTemplate, TaskType,
+    DebugRequestType, DebugTaskDefinition, ResolvedTask, RevealTarget, TaskContext, TaskModal,
+    TaskTemplate, TaskType,
 };
 use ui::{
     div, h_flex, v_flex, ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color,
@@ -320,15 +321,11 @@ impl PickerDelegate for TasksModalDelegate {
         self.workspace
             .update(cx, |workspace, cx| {
                 match task.task_type() {
-                    TaskType::Script => schedule_resolved_task(
-                        workspace,
-                        task_source_kind,
-                        task,
-                        omit_history_entry,
-                        cx,
-                    ),
-                    TaskType::Debug(_) => {
-                        let Some(config) = task.resolved_debug_adapter_config() else {
+                    TaskType::Debug(config) if config.locator.is_none() => {
+                        let Some(config): Option<DebugTaskDefinition> = task
+                            .resolved_debug_adapter_config()
+                            .and_then(|config| config.try_into().ok())
+                        else {
                             return;
                         };
                         let project = workspace.project().clone();
@@ -355,6 +352,13 @@ impl PickerDelegate for TasksModalDelegate {
                             }
                         }
                     }
+                    _ => schedule_resolved_task(
+                        workspace,
+                        task_source_kind,
+                        task,
+                        omit_history_entry,
+                        cx,
+                    ),
                 };
             })
             .ok();
@@ -517,16 +521,30 @@ impl PickerDelegate for TasksModalDelegate {
                         omit_history_entry,
                         cx,
                     ),
-                    // TODO: Should create a schedule_resolved_debug_task function
+                    // todo(debugger): Should create a schedule_resolved_debug_task function
                     // This would allow users to access to debug history and other issues
-                    TaskType::Debug(_) => workspace.project().update(cx, |project, cx| {
-                        project
-                            .start_debug_session(
-                                task.resolved_debug_adapter_config().unwrap().into(),
+                    TaskType::Debug(debug_args) => {
+                        let Some(debug_config) = task.resolved_debug_adapter_config() else {
+                            // todo(debugger) log an error, this should never happen
+                            return;
+                        };
+
+                        if debug_args.locator.is_some() {
+                            schedule_resolved_task(
+                                workspace,
+                                task_source_kind,
+                                task,
+                                omit_history_entry,
                                 cx,
-                            )
-                            .detach_and_log_err(cx);
-                    }),
+                            );
+                        } else {
+                            workspace.project().update(cx, |project, cx| {
+                                project
+                                    .start_debug_session(debug_config, cx)
+                                    .detach_and_log_err(cx);
+                            });
+                        }
+                    }
                 };
             })
             .ok();

crates/terminal/src/terminal.rs 🔗

@@ -110,6 +110,7 @@ pub enum Event {
     SelectionsChanged,
     NewNavigationTarget(Option<MaybeNavigationTarget>),
     Open(MaybeNavigationTarget),
+    TaskLocatorReady { task_id: TaskId, success: bool },
 }
 
 #[derive(Clone, Debug)]
@@ -1899,6 +1900,11 @@ impl Terminal {
             unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) };
         }
 
+        cx.emit(Event::TaskLocatorReady {
+            task_id: task.id.clone(),
+            success: finished_successfully,
+        });
+
         match task.hide {
             HideStrategy::Never => {}
             HideStrategy::Always => {

crates/terminal_view/src/terminal_view.rs 🔗

@@ -982,6 +982,15 @@ fn subscribe_for_terminal_events(
                 window.invalidate_character_coordinates();
                 cx.emit(SearchEvent::ActiveMatchChanged)
             }
+            Event::TaskLocatorReady { task_id, success } => {
+                if *success {
+                    workspace
+                        .update(cx, |workspace, cx| {
+                            workspace.debug_task_ready(task_id, cx);
+                        })
+                        .log_err();
+                }
+            }
         },
     );
     vec![terminal_subscription, terminal_events_subscription]

crates/vim/src/command.rs 🔗

@@ -1440,7 +1440,6 @@ impl ShellExec {
                         reveal_target: RevealTarget::Dock,
                         hide: HideStrategy::Never,
                         shell,
-                        program: None,
                         show_summary: false,
                         show_command: false,
                         show_rerun: false,

crates/workspace/src/tasks.rs 🔗

@@ -46,7 +46,15 @@ pub fn schedule_resolved_task(
     omit_history: bool,
     cx: &mut Context<Workspace>,
 ) {
+    let debug_config = resolved_task.resolved_debug_adapter_config();
+
     if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
+        if let Some(debug_config) = debug_config {
+            workspace
+                .debug_task_queue
+                .insert(resolved_task.id.clone(), debug_config);
+        }
+
         if !omit_history {
             resolved_task.resolved = Some(spawn_in_terminal.clone());
             workspace.project().update(cx, |project, cx| {

crates/workspace/src/workspace.rs 🔗

@@ -94,7 +94,7 @@ use std::{
     sync::{atomic::AtomicUsize, Arc, LazyLock, Weak},
     time::Duration,
 };
-use task::SpawnInTerminal;
+use task::{DebugAdapterConfig, SpawnInTerminal, TaskId};
 use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
@@ -874,6 +874,7 @@ pub struct Workspace {
     serialized_ssh_project: Option<SerializedSshProject>,
     _items_serializer: Task<Result<()>>,
     session_id: Option<String>,
+    debug_task_queue: HashMap<task::TaskId, DebugAdapterConfig>,
 }
 
 impl EventEmitter<Event> for Workspace {}
@@ -1185,6 +1186,7 @@ impl Workspace {
             _items_serializer,
             session_id: Some(session_id),
             serialized_ssh_project: None,
+            debug_task_queue: Default::default(),
         }
     }
 
@@ -5191,6 +5193,16 @@ impl Workspace {
             .update(cx, |_, window, _| window.activate_window())
             .ok();
     }
+
+    pub fn debug_task_ready(&mut self, task_id: &TaskId, cx: &mut App) {
+        if let Some(debug_config) = self.debug_task_queue.remove(task_id) {
+            self.project.update(cx, |project, cx| {
+                project
+                    .start_debug_session(debug_config, cx)
+                    .detach_and_log_err(cx);
+            })
+        }
+    }
 }
 
 fn leader_border_for_pane(