debugger/tasks: Remove TaskType enum (#29208)

Piotr Osiewicz , Cole Miller , Anthony Eid , Conrad Irwin , Anthony , and Conrad created

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <m@cole-miller.net>
Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>

Change summary

crates/collab/src/db.rs                               |   2 
crates/collab/src/db/tables/worktree_settings_file.rs |   2 
crates/collab/src/tests/editor_tests.rs               |   1 
crates/collab/src/tests/integration_tests.rs          |   2 
crates/dap/src/adapters.rs                            | 122 +++++
crates/dap/src/dap.rs                                 |   2 
crates/dap/src/registry.rs                            |  25 +
crates/dap_adapters/src/codelldb.rs                   |   9 
crates/dap_adapters/src/gdb.rs                        |   4 
crates/dap_adapters/src/go.rs                         |   3 
crates/dap_adapters/src/javascript.rs                 |   4 
crates/dap_adapters/src/php.rs                        |   3 
crates/dap_adapters/src/python.rs                     |   6 
crates/debugger_ui/src/attach_modal.rs                |  23 
crates/debugger_ui/src/debugger_panel.rs              | 191 +++++---
crates/debugger_ui/src/new_session_modal.rs           |  97 +--
crates/debugger_ui/src/session.rs                     |   8 
crates/debugger_ui/src/tests.rs                       |  15 
crates/debugger_ui/src/tests/attach_modal.rs          |   9 
crates/editor/src/actions.rs                          |   4 
crates/editor/src/code_context_menus.rs               | 163 +++---
crates/editor/src/editor.rs                           | 112 +++-
crates/editor/src/mouse_context_menu.rs               |   1 
crates/language/src/task_context.rs                   |   3 
crates/languages/src/go.rs                            |   4 
crates/languages/src/python.rs                        |   4 
crates/languages/src/rust.rs                          |  68 +-
crates/project/src/debugger/dap_store.rs              | 157 ++++--
crates/project/src/debugger/locator_store.rs          |  34 -
crates/project/src/debugger/locators.rs               |   9 
crates/project/src/debugger/locators/cargo.rs         |  57 +-
crates/project/src/debugger/session.rs                |  30 -
crates/project/src/project.rs                         |   4 
crates/project/src/project_settings.rs                |  93 ++-
crates/project/src/project_tests.rs                   |   7 
crates/project/src/task_inventory.rs                  | 288 +++++++++---
crates/project/src/task_store.rs                      |  24 
crates/proto/proto/debugger.proto                     |  33 +
crates/proto/proto/worktree.proto                     |   1 
crates/proto/proto/zed.proto                          |   4 
crates/proto/src/proto.rs                             |   8 
crates/remote_server/src/headless_project.rs          |   2 
crates/settings/src/settings.rs                       |   2 
crates/settings/src/settings_store.rs                 |  36 
crates/task/src/debug_format.rs                       | 226 +++------
crates/task/src/lib.rs                                | 104 +---
crates/task/src/task_template.rs                      |  57 --
crates/task/src/vscode_debug_format.rs                |  96 ++-
crates/tasks_ui/src/modal.rs                          | 105 +---
crates/tasks_ui/src/tasks_ui.rs                       |  14 
crates/workspace/src/tasks.rs                         |  90 +--
crates/workspace/src/workspace.rs                     |  14 
crates/zed/src/zed.rs                                 |   2 
53 files changed, 1,271 insertions(+), 1,113 deletions(-)

Detailed changes

crates/collab/src/db.rs 🔗

@@ -800,6 +800,7 @@ impl LocalSettingsKind {
             proto::LocalSettingsKind::Settings => Self::Settings,
             proto::LocalSettingsKind::Tasks => Self::Tasks,
             proto::LocalSettingsKind::Editorconfig => Self::Editorconfig,
+            proto::LocalSettingsKind::Debug => Self::Debug,
         }
     }
 
@@ -808,6 +809,7 @@ impl LocalSettingsKind {
             Self::Settings => proto::LocalSettingsKind::Settings,
             Self::Tasks => proto::LocalSettingsKind::Tasks,
             Self::Editorconfig => proto::LocalSettingsKind::Editorconfig,
+            Self::Debug => proto::LocalSettingsKind::Debug,
         }
     }
 }

crates/collab/src/tests/editor_tests.rs 🔗

@@ -680,6 +680,7 @@ async fn test_collaborating_with_code_actions(
         editor.toggle_code_actions(
             &ToggleCodeActions {
                 deployed_from_indicator: None,
+                quick_launch: false,
             },
             window,
             cx,

crates/collab/src/tests/integration_tests.rs 🔗

@@ -1824,6 +1824,8 @@ async fn test_active_call_events(
     server
         .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
         .await;
+    executor.run_until_parked();
+
     let active_call_a = cx_a.read(ActiveCall::global);
     let active_call_b = cx_b.read(ActiveCall::global);
 

crates/dap/src/adapters.rs 🔗

@@ -14,10 +14,16 @@ use serde::{Deserialize, Serialize};
 use settings::WorktreeId;
 use smol::{self, fs::File, lock::Mutex};
 use std::{
-    borrow::Borrow, collections::HashSet, ffi::OsStr, fmt::Debug, net::Ipv4Addr, ops::Deref,
-    path::PathBuf, sync::Arc,
+    borrow::Borrow,
+    collections::HashSet,
+    ffi::OsStr,
+    fmt::Debug,
+    net::Ipv4Addr,
+    ops::Deref,
+    path::{Path, PathBuf},
+    sync::Arc,
 };
-use task::{DebugTaskDefinition, TcpArgumentsTemplate};
+use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
 use util::ResultExt;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -109,6 +115,116 @@ impl TcpArguments {
     }
 }
 
+/// Represents a debuggable binary/process (what process is going to be debugged and with what arguments).
+///
+/// We start off with a [DebugScenario], a user-facing type that additionally defines how a debug target is built; once
+/// an optional build step is completed, we turn it's result into a DebugTaskDefinition by running a locator (or using a user-provided task) and resolving task variables.
+/// Finally, a [DebugTaskDefinition] has to be turned into a concrete debugger invocation ([DebugAdapterBinary]).
+#[derive(Clone, Debug, PartialEq)]
+pub struct DebugTaskDefinition {
+    pub label: SharedString,
+    pub adapter: SharedString,
+    pub request: DebugRequest,
+    /// Additional initialization arguments to be sent on DAP initialization
+    pub initialize_args: Option<serde_json::Value>,
+    /// Whether to tell the debug adapter to stop on entry
+    pub stop_on_entry: Option<bool>,
+    /// Optional TCP connection information
+    ///
+    /// If provided, this will be used to connect to the debug adapter instead of
+    /// spawning a new debug adapter process. This is useful for connecting to a debug adapter
+    /// that is already running or is started by another process.
+    pub tcp_connection: Option<TcpArgumentsTemplate>,
+}
+
+impl DebugTaskDefinition {
+    pub fn cwd(&self) -> Option<&Path> {
+        if let DebugRequest::Launch(config) = &self.request {
+            config.cwd.as_ref().map(Path::new)
+        } else {
+            None
+        }
+    }
+
+    pub fn to_scenario(&self) -> DebugScenario {
+        DebugScenario {
+            label: self.label.clone(),
+            adapter: self.adapter.clone(),
+            build: None,
+            request: Some(self.request.clone()),
+            stop_on_entry: self.stop_on_entry,
+            tcp_connection: self.tcp_connection.clone(),
+            initialize_args: self.initialize_args.clone(),
+        }
+    }
+
+    pub fn to_proto(&self) -> proto::DebugTaskDefinition {
+        proto::DebugTaskDefinition {
+            adapter: self.adapter.to_string(),
+            request: Some(match &self.request {
+                DebugRequest::Launch(config) => {
+                    proto::debug_task_definition::Request::DebugLaunchRequest(
+                        proto::DebugLaunchRequest {
+                            program: config.program.clone(),
+                            cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
+                            args: config.args.clone(),
+                            env: config
+                                .env
+                                .iter()
+                                .map(|(k, v)| (k.clone(), v.clone()))
+                                .collect(),
+                        },
+                    )
+                }
+                DebugRequest::Attach(attach_request) => {
+                    proto::debug_task_definition::Request::DebugAttachRequest(
+                        proto::DebugAttachRequest {
+                            process_id: attach_request.process_id.unwrap_or_default(),
+                        },
+                    )
+                }
+            }),
+            label: self.label.to_string(),
+            initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
+            tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
+            stop_on_entry: self.stop_on_entry,
+        }
+    }
+
+    pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
+        let request = proto
+            .request
+            .ok_or_else(|| anyhow::anyhow!("request is required"))?;
+        Ok(Self {
+            label: proto.label.into(),
+            initialize_args: proto.initialize_args.map(|v| v.into()),
+            tcp_connection: proto
+                .tcp_connection
+                .map(TcpArgumentsTemplate::from_proto)
+                .transpose()?,
+            stop_on_entry: proto.stop_on_entry,
+            adapter: proto.adapter.into(),
+            request: match request {
+                proto::debug_task_definition::Request::DebugAttachRequest(config) => {
+                    DebugRequest::Attach(AttachRequest {
+                        process_id: Some(config.process_id),
+                    })
+                }
+
+                proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
+                    DebugRequest::Launch(LaunchRequest {
+                        program: config.program,
+                        cwd: config.cwd.map(|cwd| cwd.into()),
+                        args: config.args,
+                        env: Default::default(),
+                    })
+                }
+            },
+        })
+    }
+}
+
+/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
 #[derive(Debug, Clone)]
 pub struct DebugAdapterBinary {
     pub command: String,

crates/dap/src/dap.rs 🔗

@@ -6,7 +6,7 @@ mod registry;
 pub mod transport;
 
 pub use dap_types::*;
-pub use registry::DapRegistry;
+pub use registry::{DapLocator, DapRegistry};
 pub use task::DebugRequest;
 
 pub type ScopeId = u64;

crates/dap/src/registry.rs 🔗

@@ -1,12 +1,25 @@
+use anyhow::Result;
+use async_trait::async_trait;
+use collections::FxHashMap;
 use gpui::{App, Global};
 use parking_lot::RwLock;
+use task::{DebugRequest, SpawnInTerminal};
 
 use crate::adapters::{DebugAdapter, DebugAdapterName};
 use std::{collections::BTreeMap, sync::Arc};
 
+/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
+#[async_trait]
+pub trait DapLocator: Send + Sync {
+    /// Determines whether this locator can generate debug target for given task.
+    fn accepts(&self, build_config: &SpawnInTerminal) -> bool;
+    async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>;
+}
+
 #[derive(Default)]
 struct DapRegistryState {
     adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
+    locators: FxHashMap<String, Arc<dyn DapLocator>>,
 }
 
 #[derive(Clone, Default)]
@@ -35,6 +48,18 @@ impl DapRegistry {
         );
     }
 
+    pub fn add_locator(&self, name: String, locator: Arc<dyn DapLocator>) {
+        let _previous_value = self.0.write().locators.insert(name, locator);
+        debug_assert!(
+            _previous_value.is_none(),
+            "Attempted to insert a new debug locator when one is already registered"
+        );
+    }
+
+    pub fn locators(&self) -> FxHashMap<String, Arc<dyn DapLocator>> {
+        self.0.read().locators.clone()
+    }
+
     pub fn adapter(&self, name: &str) -> Option<Arc<dyn DebugAdapter>> {
         self.0.read().adapters.get(name).cloned()
     }

crates/dap_adapters/src/codelldb.rs 🔗

@@ -2,9 +2,9 @@ use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
 
 use anyhow::{Result, bail};
 use async_trait::async_trait;
-use dap::adapters::{InlineValueProvider, latest_github_release};
+use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release};
 use gpui::AsyncApp;
-use task::{DebugRequest, DebugTaskDefinition};
+use task::DebugRequest;
 
 use crate::*;
 
@@ -25,7 +25,10 @@ impl CodeLldbDebugAdapter {
         });
         let map = configuration.as_object_mut().unwrap();
         // CodeLLDB uses `name` for a terminal label.
-        map.insert("name".into(), Value::String(config.label.clone()));
+        map.insert(
+            "name".into(),
+            Value::String(String::from(config.label.as_ref())),
+        );
         let request = config.request.to_dap();
         match &config.request {
             DebugRequest::Attach(attach) => {

crates/dap_adapters/src/gdb.rs 🔗

@@ -2,9 +2,9 @@ use std::{collections::HashMap, ffi::OsStr};
 
 use anyhow::{Result, bail};
 use async_trait::async_trait;
-use dap::StartDebuggingRequestArguments;
+use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::AsyncApp;
-use task::{DebugRequest, DebugTaskDefinition};
+use task::DebugRequest;
 
 use crate::*;
 

crates/dap_adapters/src/go.rs 🔗

@@ -1,7 +1,6 @@
-use dap::StartDebuggingRequestArguments;
+use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::AsyncApp;
 use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
-use task::DebugTaskDefinition;
 
 use crate::*;
 

crates/dap_adapters/src/javascript.rs 🔗

@@ -1,8 +1,8 @@
 use adapters::latest_github_release;
-use dap::StartDebuggingRequestArguments;
+use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::AsyncApp;
 use std::{collections::HashMap, path::PathBuf};
-use task::{DebugRequest, DebugTaskDefinition};
+use task::DebugRequest;
 
 use crate::*;
 

crates/dap_adapters/src/php.rs 🔗

@@ -1,8 +1,7 @@
 use adapters::latest_github_release;
-use dap::adapters::TcpArguments;
+use dap::adapters::{DebugTaskDefinition, TcpArguments};
 use gpui::AsyncApp;
 use std::{collections::HashMap, path::PathBuf};
-use task::DebugTaskDefinition;
 
 use crate::*;
 

crates/dap_adapters/src/python.rs 🔗

@@ -1,8 +1,10 @@
 use crate::*;
-use dap::{StartDebuggingRequestArguments, adapters::InlineValueProvider};
+use dap::{
+    DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition,
+    adapters::InlineValueProvider,
+};
 use gpui::AsyncApp;
 use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
-use task::DebugTaskDefinition;
 
 #[derive(Default)]
 pub(crate) struct PythonDebugAdapter;

crates/debugger_ui/src/attach_modal.rs 🔗

@@ -1,4 +1,5 @@
 use dap::DebugRequest;
+use dap::adapters::DebugTaskDefinition;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{DismissEvent, Entity, EventEmitter, Focusable, Render};
 use gpui::{Subscription, WeakEntity};
@@ -24,20 +25,20 @@ pub(crate) struct AttachModalDelegate {
     selected_index: usize,
     matches: Vec<StringMatch>,
     placeholder_text: Arc<str>,
+    pub(crate) definition: DebugTaskDefinition,
     workspace: WeakEntity<Workspace>,
-    pub(crate) debug_config: task::DebugTaskDefinition,
     candidates: Arc<[Candidate]>,
 }
 
 impl AttachModalDelegate {
     fn new(
         workspace: Entity<Workspace>,
-        debug_config: task::DebugTaskDefinition,
+        definition: DebugTaskDefinition,
         candidates: Arc<[Candidate]>,
     ) -> Self {
         Self {
             workspace: workspace.downgrade(),
-            debug_config,
+            definition,
             candidates,
             selected_index: 0,
             matches: Vec::default(),
@@ -53,8 +54,8 @@ pub struct AttachModal {
 
 impl AttachModal {
     pub fn new(
+        definition: DebugTaskDefinition,
         workspace: Entity<Workspace>,
-        debug_config: task::DebugTaskDefinition,
         modal: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -77,12 +78,12 @@ impl AttachModal {
             .collect();
         processes.sort_by_key(|k| k.name.clone());
         let processes = processes.into_iter().collect();
-        Self::with_processes(workspace, debug_config, processes, modal, window, cx)
+        Self::with_processes(workspace, definition, processes, modal, window, cx)
     }
 
     pub(super) fn with_processes(
         workspace: Entity<Workspace>,
-        debug_config: task::DebugTaskDefinition,
+        definition: DebugTaskDefinition,
         processes: Arc<[Candidate]>,
         modal: bool,
         window: &mut Window,
@@ -90,7 +91,7 @@ impl AttachModal {
     ) -> Self {
         let picker = cx.new(|cx| {
             Picker::uniform_list(
-                AttachModalDelegate::new(workspace, debug_config, processes),
+                AttachModalDelegate::new(workspace, definition, processes),
                 window,
                 cx,
             )
@@ -217,7 +218,7 @@ impl PickerDelegate for AttachModalDelegate {
             return cx.emit(DismissEvent);
         };
 
-        match &mut self.debug_config.request {
+        match &mut self.definition.request {
             DebugRequest::Attach(config) => {
                 config.process_id = Some(candidate.pid);
             }
@@ -227,7 +228,8 @@ impl PickerDelegate for AttachModalDelegate {
             }
         }
 
-        let definition = self.debug_config.clone();
+        let scenario = self.definition.to_scenario();
+
         let panel = self
             .workspace
             .update(cx, |workspace, cx| workspace.panel::<DebugPanel>(cx))
@@ -235,9 +237,10 @@ impl PickerDelegate for AttachModalDelegate {
             .flatten();
         if let Some(panel) = panel {
             panel.update(cx, |panel, cx| {
-                panel.start_session(definition, window, cx);
+                panel.start_session(scenario, Default::default(), None, window, cx);
             });
         }
+
         cx.emit(DismissEvent);
     }
 

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -9,11 +9,12 @@ use crate::{new_session_modal::NewSessionModal, session::DebugSession};
 use anyhow::{Result, anyhow};
 use collections::HashMap;
 use command_palette_hooks::CommandPaletteFilter;
-use dap::StartDebuggingRequestArguments;
+use dap::DebugRequest;
 use dap::{
     ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
     client::SessionId, debugger_settings::DebuggerSettings,
 };
+use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use futures::{SinkExt as _, channel::mpsc};
 use gpui::{
     Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
@@ -21,6 +22,7 @@ use gpui::{
     actions, anchored, deferred,
 };
 
+use language::Buffer;
 use project::debugger::session::{Session, SessionStateEvent};
 use project::{
     Project,
@@ -35,9 +37,7 @@ use settings::Settings;
 use std::any::TypeId;
 use std::path::Path;
 use std::sync::Arc;
-use task::{
-    DebugTaskDefinition, DebugTaskTemplate, HideStrategy, RevealStrategy, RevealTarget, TaskId,
-};
+use task::{DebugScenario, HideStrategy, RevealStrategy, RevealTarget, TaskContext, TaskId};
 use terminal_view::TerminalView;
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 use workspace::SplitDirection;
@@ -87,45 +87,8 @@ impl DebugPanel {
             let project = workspace.project().clone();
             let dap_store = project.read(cx).dap_store();
 
-            let weak = cx.weak_entity();
-
-            let modal_subscription =
-                cx.observe_new::<tasks_ui::TasksModal>(move |_, window, cx| {
-                    let modal_entity = cx.entity();
-
-                    weak.update(cx, |_: &mut DebugPanel, cx| {
-                        let Some(window) = window else {
-                            log::error!("Debug panel couldn't subscribe to tasks modal because there was no window");
-                            return;
-                        };
-
-                        cx.subscribe_in(
-                            &modal_entity,
-                            window,
-                            |panel, _, event: &tasks_ui::ShowAttachModal, window, cx| {
-                                panel.workspace.update(cx, |workspace, cx| {
-                                    let workspace_handle = cx.entity().clone();
-                                    workspace.toggle_modal(window, cx, |window, cx| {
-                                        crate::attach_modal::AttachModal::new(
-                                            workspace_handle,
-                                            event.debug_config.clone(),
-                                            true,
-                                            window,
-                                            cx,
-                                        )
-                                    });
-                                }).ok();
-                            },
-                        )
-                        .detach();
-                    })
-                    .ok();
-                });
-
-            let _subscriptions = vec![
-                cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event),
-                modal_subscription,
-            ];
+            let _subscriptions =
+                vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
 
             let debug_panel = Self {
                 size: px(300.),
@@ -259,43 +222,16 @@ impl DebugPanel {
         })
     }
 
-    pub fn start_session(
+    fn start_from_definition(
         &mut self,
         definition: DebugTaskDefinition,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) {
-        let task_contexts = self
-            .workspace
-            .update(cx, |workspace, cx| {
-                tasks_ui::task_contexts(workspace, window, cx)
-            })
-            .ok();
-        let dap_store = self.project.read(cx).dap_store().clone();
-
+    ) -> Task<Result<()>> {
         cx.spawn_in(window, async move |this, cx| {
-            let task_context = if let Some(task) = task_contexts {
-                task.await
-                    .active_worktree_context
-                    .map_or(task::TaskContext::default(), |context| context.1)
-            } else {
-                task::TaskContext::default()
-            };
-
+            let dap_store = this.update(cx, |this, cx| this.project.read(cx).dap_store())?;
             let (session, task) = dap_store.update(cx, |dap_store, cx| {
-                let template = DebugTaskTemplate {
-                    locator: None,
-                    definition: definition.clone(),
-                };
-                let session = if let Some(debug_config) = template
-                    .to_zed_format()
-                    .resolve_task("debug_task", &task_context)
-                    .and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
-                {
-                    dap_store.new_session(debug_config.definition, None, cx)
-                } else {
-                    dap_store.new_session(definition.clone(), None, cx)
-                };
+                let session = dap_store.new_session(definition, None, cx);
 
                 (session.clone(), dap_store.boot_session(session, cx))
             })?;
@@ -318,6 +254,27 @@ impl DebugPanel {
 
             anyhow::Ok(())
         })
+    }
+
+    pub fn start_session(
+        &mut self,
+        scenario: DebugScenario,
+        task_context: TaskContext,
+        active_buffer: Option<Entity<Buffer>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        cx.spawn_in(window, async move |this, cx| {
+            let definition = this
+                .update_in(cx, |this, window, cx| {
+                    this.resolve_scenario(scenario, task_context, active_buffer, window, cx)
+                })?
+                .await?;
+            this.update_in(cx, |this, window, cx| {
+                this.start_from_definition(definition, window, cx)
+            })?
+            .await
+        })
         .detach_and_log_err(cx);
     }
 
@@ -343,13 +300,13 @@ impl DebugPanel {
                         let definition = curr_session.update(cx, |session, _| session.definition());
                         let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
 
-                        let definition = definition.clone();
                         cx.spawn_in(window, async move |this, cx| {
                             task.await;
 
                             this.update_in(cx, |this, window, cx| {
-                                this.start_session(definition, window, cx)
-                            })
+                                this.start_from_definition(definition, window, cx)
+                            })?
+                            .await
                         })
                         .detach_and_log_err(cx);
                     }
@@ -503,6 +460,75 @@ impl DebugPanel {
         }
     }
 
+    pub fn resolve_scenario(
+        &self,
+        scenario: DebugScenario,
+
+        task_context: TaskContext,
+        buffer: Option<Entity<Buffer>>,
+        window: &Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<DebugTaskDefinition>> {
+        let project = self.project.read(cx);
+        let dap_store = project.dap_store().downgrade();
+        let task_store = project.task_store().downgrade();
+        let workspace = self.workspace.clone();
+        cx.spawn_in(window, async move |_, cx| {
+            let DebugScenario {
+                adapter,
+                label,
+                build,
+                request,
+                initialize_args,
+                tcp_connection,
+                stop_on_entry,
+            } = scenario;
+            let request = if let Some(mut request) = request {
+                // Resolve task variables within the request.
+                if let DebugRequest::Launch(_) = &mut request {}
+
+                request
+            } else if let Some(build) = build {
+                let Some(task) = task_store.update(cx, |this, cx| {
+                    this.task_inventory().and_then(|inventory| {
+                        inventory
+                            .read(cx)
+                            .task_template_by_label(buffer, &build, cx)
+                    })
+                })?
+                else {
+                    anyhow::bail!("Couldn't find task template for {:?}", build)
+                };
+                let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
+                    anyhow::bail!("Could not resolve task variables within a debug scenario");
+                };
+
+                let run_build = workspace.update_in(cx, |workspace, window, cx| {
+                    workspace.spawn_in_terminal(task.resolved.clone(), window, cx)
+                })?;
+
+                let exit_status = run_build.await?;
+                if !exit_status.success() {
+                    anyhow::bail!("Build failed");
+                }
+
+                dap_store
+                    .update(cx, |this, cx| this.run_debug_locator(task.resolved, cx))?
+                    .await?
+            } else {
+                return Err(anyhow!("No request or build provided"));
+            };
+            Ok(DebugTaskDefinition {
+                label,
+                adapter,
+                request,
+                initialize_args,
+                stop_on_entry,
+                tcp_connection,
+            })
+        })
+    }
+
     fn handle_run_in_terminal_request(
         &self,
         session_id: SessionId,
@@ -1409,10 +1435,17 @@ impl Render for DebugPanel {
 struct DebuggerProvider(Entity<DebugPanel>);
 
 impl workspace::DebuggerProvider for DebuggerProvider {
-    fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App) {
+    fn start_session(
+        &self,
+        definition: DebugScenario,
+        context: TaskContext,
+        buffer: Option<Entity<Buffer>>,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
         self.0.update(cx, |_, cx| {
             cx.defer_in(window, |this, window, cx| {
-                this.start_session(definition, window, cx);
+                this.start_session(definition, context, buffer, window, cx);
             })
         })
     }

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -4,14 +4,14 @@ use std::{
     path::{Path, PathBuf},
 };
 
-use dap::{DapRegistry, DebugRequest};
+use dap::{DapRegistry, DebugRequest, adapters::DebugTaskDefinition};
 use editor::{Editor, EditorElement, EditorStyle};
 use gpui::{
     App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
     WeakEntity,
 };
 use settings::Settings;
-use task::{DebugTaskDefinition, DebugTaskTemplate, LaunchRequest};
+use task::{DebugScenario, LaunchRequest, TaskContext};
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@@ -34,7 +34,7 @@ pub(super) struct NewSessionModal {
     last_selected_profile_name: Option<SharedString>,
 }
 
-fn suggested_label(request: &DebugRequest, debugger: &str) -> String {
+fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
     match request {
         DebugRequest::Launch(config) => {
             let last_path_component = Path::new(&config.program)
@@ -42,12 +42,13 @@ fn suggested_label(request: &DebugRequest, debugger: &str) -> String {
                 .map(|name| name.to_string_lossy())
                 .unwrap_or_else(|| Cow::Borrowed(&config.program));
 
-            format!("{} ({debugger})", last_path_component)
+            format!("{} ({debugger})", last_path_component).into()
         }
         DebugRequest::Attach(config) => format!(
             "pid: {} ({debugger})",
             config.process_id.unwrap_or(u32::MAX)
-        ),
+        )
+        .into(),
     }
 }
 
@@ -61,7 +62,7 @@ impl NewSessionModal {
     ) -> Self {
         let debugger = past_debug_definition
             .as_ref()
-            .map(|def| def.adapter.clone().into());
+            .map(|def| def.adapter.clone());
 
         let stop_on_entry = past_debug_definition
             .as_ref()
@@ -85,18 +86,20 @@ impl NewSessionModal {
         }
     }
 
-    fn debug_config(&self, cx: &App, debugger: &str) -> DebugTaskDefinition {
+    fn debug_config(&self, cx: &App, debugger: &str) -> DebugScenario {
         let request = self.mode.debug_task(cx);
-        DebugTaskDefinition {
-            adapter: debugger.to_owned(),
-            label: suggested_label(&request, debugger),
-            request,
+        let label = suggested_label(&request, debugger);
+        DebugScenario {
+            adapter: debugger.to_owned().into(),
+            label,
+            request: Some(request),
             initialize_args: self.initialize_args.clone(),
             tcp_connection: None,
             stop_on_entry: match self.stop_on_entry {
                 ToggleState::Selected => Some(true),
                 _ => None,
             },
+            build: None,
         }
     }
 
@@ -109,36 +112,9 @@ impl NewSessionModal {
         let config = self.debug_config(cx, debugger);
         let debug_panel = self.debug_panel.clone();
 
-        let task_contexts = self
-            .workspace
-            .update(cx, |workspace, cx| {
-                tasks_ui::task_contexts(workspace, window, cx)
-            })
-            .ok();
-
         cx.spawn_in(window, async move |this, cx| {
-            let task_context = if let Some(task) = task_contexts {
-                task.await
-                    .active_worktree_context
-                    .map_or(task::TaskContext::default(), |context| context.1)
-            } else {
-                task::TaskContext::default()
-            };
-
             debug_panel.update_in(cx, |debug_panel, window, cx| {
-                let template = DebugTaskTemplate {
-                    locator: None,
-                    definition: config.clone(),
-                };
-                if let Some(debug_config) = template
-                    .to_zed_format()
-                    .resolve_task("debug_task", &task_context)
-                    .and_then(|resolved_task| resolved_task.resolved_debug_adapter_config())
-                {
-                    debug_panel.start_session(debug_config.definition, window, cx)
-                } else {
-                    debug_panel.start_session(config, window, cx)
-                }
+                debug_panel.start_session(config, TaskContext::default(), None, window, cx)
             })?;
             this.update(cx, |_, cx| {
                 cx.emit(DismissEvent);
@@ -156,12 +132,13 @@ impl NewSessionModal {
         cx: &mut App,
     ) {
         attach.update(cx, |this, cx| {
-            if selected_debugger != this.debug_definition.adapter {
-                this.debug_definition.adapter = selected_debugger.into();
+            if selected_debugger != this.definition.adapter.as_ref() {
+                let adapter: SharedString = selected_debugger.to_owned().into();
+                this.definition.adapter = adapter.clone();
 
                 this.attach_picker.update(cx, |this, cx| {
                     this.picker.update(cx, |this, cx| {
-                        this.delegate.debug_config.adapter = selected_debugger.into();
+                        this.delegate.definition.adapter = adapter;
                         this.focus(window, cx);
                     })
                 });
@@ -224,22 +201,22 @@ impl NewSessionModal {
             "debug-config-menu",
             last_profile.unwrap_or_else(|| SELECT_SCENARIO_LABEL.clone()),
             ContextMenu::build(window, cx, move |mut menu, _, cx| {
-                let setter_for_name = |task: DebugTaskDefinition| {
+                let setter_for_name = |task: DebugScenario| {
                     let weak = weak.clone();
                     move |window: &mut Window, cx: &mut App| {
                         weak.update(cx, |this, cx| {
                             this.last_selected_profile_name = Some(SharedString::from(&task.label));
-                            this.debugger = Some(task.adapter.clone().into());
+                            this.debugger = Some(task.adapter.clone());
                             this.initialize_args = task.initialize_args.clone();
                             match &task.request {
-                                DebugRequest::Launch(launch_config) => {
+                                Some(DebugRequest::Launch(launch_config)) => {
                                     this.mode = NewSessionMode::launch(
                                         Some(launch_config.clone()),
                                         window,
                                         cx,
                                     );
                                 }
-                                DebugRequest::Attach(_) => {
+                                Some(DebugRequest::Attach(_)) => {
                                     let Some(workspace) = this.workspace.upgrade() else {
                                         return;
                                     };
@@ -256,6 +233,7 @@ impl NewSessionModal {
                                         Self::update_attach_picker(&attach, &debugger, window, cx);
                                     }
                                 }
+                                _ => log::warn!("Selected debug scenario without either attach or launch request specified"),
                             }
                             cx.notify();
                         })
@@ -263,7 +241,7 @@ impl NewSessionModal {
                     }
                 };
 
-                let available_adapters: Vec<DebugTaskTemplate> = workspace
+                let available_tasks: Vec<DebugScenario> = workspace
                     .update(cx, |this, cx| {
                         this.project()
                             .read(cx)
@@ -271,19 +249,19 @@ impl NewSessionModal {
                             .read(cx)
                             .task_inventory()
                             .iter()
-                            .flat_map(|task_inventory| task_inventory.read(cx).list_debug_tasks())
-                            .cloned()
-                            .filter_map(|task| task.try_into().ok())
+                            .flat_map(|task_inventory| {
+                                task_inventory.read(cx).list_debug_scenarios(None)
+                            })
                             .collect()
                     })
                     .ok()
                     .unwrap_or_default();
 
-                for debug_definition in available_adapters {
+                for debug_definition in available_tasks {
                     menu = menu.entry(
-                        debug_definition.definition.label.clone(),
+                        debug_definition.label.clone(),
                         None,
-                        setter_for_name(debug_definition.definition),
+                        setter_for_name(debug_definition),
                     );
                 }
                 menu
@@ -332,13 +310,14 @@ impl LaunchMode {
             program: self.program.read(cx).text(cx),
             cwd: path.is_empty().not().then(|| PathBuf::from(path)),
             args: Default::default(),
+            env: Default::default(),
         }
     }
 }
 
 #[derive(Clone)]
 struct AttachMode {
-    debug_definition: DebugTaskDefinition,
+    definition: DebugTaskDefinition,
     attach_picker: Entity<AttachModal>,
 }
 
@@ -349,22 +328,22 @@ impl AttachMode {
         window: &mut Window,
         cx: &mut Context<NewSessionModal>,
     ) -> Entity<Self> {
-        let debug_definition = DebugTaskDefinition {
+        let definition = DebugTaskDefinition {
+            adapter: debugger.clone().unwrap_or_default(),
             label: "Attach New Session Setup".into(),
             request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
-            tcp_connection: None,
-            adapter: debugger.clone().unwrap_or_default().into(),
             initialize_args: None,
+            tcp_connection: None,
             stop_on_entry: Some(false),
         };
         let attach_picker = cx.new(|cx| {
-            let modal = AttachModal::new(workspace, debug_definition.clone(), false, window, cx);
+            let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
             window.focus(&modal.focus_handle(cx));
 
             modal
         });
         cx.new(|_| Self {
-            debug_definition,
+            definition,
             attach_picker,
         })
     }

crates/debugger_ui/src/session.rs 🔗

@@ -33,7 +33,7 @@ impl DebugSessionState {
 pub struct DebugSession {
     remote_id: Option<workspace::ViewId>,
     mode: DebugSessionState,
-    label: OnceLock<String>,
+    label: OnceLock<SharedString>,
     dap_store: WeakEntity<DapStore>,
     _debug_panel: WeakEntity<DebugPanel>,
     _worktree_store: WeakEntity<WorktreeStore>,
@@ -110,9 +110,9 @@ impl DebugSession {
         }
     }
 
-    pub(crate) fn label(&self, cx: &App) -> String {
+    pub(crate) fn label(&self, cx: &App) -> SharedString {
         if let Some(label) = self.label.get() {
-            return label.to_owned();
+            return label.clone();
         }
 
         let session_id = match &self.mode {
@@ -123,7 +123,7 @@ impl DebugSession {
             .dap_store
             .read_with(cx, |store, _| store.session_by_id(session_id))
         else {
-            return "".to_owned();
+            return "".into();
         };
 
         self.label

crates/debugger_ui/src/tests.rs 🔗

@@ -1,11 +1,12 @@
 use std::sync::Arc;
 
 use anyhow::{Result, anyhow};
+use dap::adapters::DebugTaskDefinition;
 use dap::{DebugRequest, client::DebugAdapterClient};
 use gpui::{Entity, TestAppContext, WindowHandle};
 use project::{Project, debugger::session::Session};
 use settings::SettingsStore;
-use task::DebugTaskDefinition;
+use task::TaskContext;
 use terminal_view::terminal_panel::TerminalPanel;
 use workspace::Workspace;
 
@@ -104,7 +105,13 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
 ) -> Result<Entity<Session>> {
     let _subscription = project::debugger::test::intercept_debug_sessions(cx, configure);
     workspace.update(cx, |workspace, window, cx| {
-        workspace.start_debug_session(config, window, cx)
+        workspace.start_debug_session(
+            config.to_scenario(),
+            TaskContext::default(),
+            None,
+            window,
+            cx,
+        )
     })?;
     cx.run_until_parked();
     let session = workspace.read_with(cx, |workspace, cx| {
@@ -128,9 +135,9 @@ pub fn start_debug_session<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
         workspace,
         cx,
         DebugTaskDefinition {
-            adapter: "fake-adapter".to_string(),
+            adapter: "fake-adapter".into(),
             request: DebugRequest::Launch(Default::default()),
-            label: "test".to_string(),
+            label: "test".into(),
             initialize_args: None,
             tcp_connection: None,
             stop_on_entry: None,

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

@@ -1,11 +1,11 @@
 use crate::{attach_modal::Candidate, tests::start_debug_session_with, *};
 use attach_modal::AttachModal;
-use dap::FakeAdapter;
+use dap::{FakeAdapter, adapters::DebugTaskDefinition};
 use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
 use menu::Confirm;
 use project::{FakeFs, Project};
 use serde_json::json;
-use task::{AttachRequest, DebugTaskDefinition, TcpArgumentsTemplate};
+use task::{AttachRequest, TcpArgumentsTemplate};
 use tests::{init_test, init_test_workspace};
 
 #[gpui::test]
@@ -30,11 +30,11 @@ async fn test_direct_attach_to_process(executor: BackgroundExecutor, cx: &mut Te
         &workspace,
         cx,
         DebugTaskDefinition {
-            adapter: "fake-adapter".to_string(),
+            adapter: "fake-adapter".into(),
             request: dap::DebugRequest::Attach(AttachRequest {
                 process_id: Some(10),
             }),
-            label: "label".to_string(),
+            label: "label".into(),
             initialize_args: None,
             tcp_connection: None,
             stop_on_entry: None,
@@ -104,6 +104,7 @@ async fn test_show_attach_modal_and_select_process(
                     workspace_handle,
                     DebugTaskDefinition {
                         adapter: FakeAdapter::ADAPTER_NAME.into(),
+
                         request: dap::DebugRequest::Attach(AttachRequest::default()),
                         label: "attach example".into(),
                         initialize_args: None,

crates/editor/src/actions.rs 🔗

@@ -78,6 +78,10 @@ pub struct ToggleCodeActions {
     #[serde(default)]
     #[serde(skip)]
     pub deployed_from_indicator: Option<DisplayRow>,
+    // Run first available task if there is only one.
+    #[serde(default)]
+    #[serde(skip)]
+    pub quick_launch: bool,
 }
 
 #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema)]

crates/editor/src/code_context_menus.rs 🔗

@@ -1,4 +1,3 @@
-use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt as _};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     AnyElement, BackgroundExecutor, Entity, Focusable, FontWeight, ListSizingBehavior,
@@ -13,6 +12,8 @@ use ordered_float::OrderedFloat;
 use project::CompletionSource;
 use project::lsp_store::CompletionDocumentation;
 use project::{CodeAction, Completion, TaskSourceKind};
+use task::DebugScenario;
+use task::TaskContext;
 
 use std::{
     cell::RefCell,
@@ -39,6 +40,7 @@ pub const MENU_ASIDE_X_PADDING: Pixels = px(16.);
 pub const MENU_ASIDE_MIN_WIDTH: Pixels = px(260.);
 pub const MENU_ASIDE_MAX_WIDTH: Pixels = px(500.);
 
+#[allow(clippy::large_enum_variant)]
 pub enum CodeContextMenu {
     Completions(CompletionsMenu),
     CodeActions(CodeActionsMenu),
@@ -819,28 +821,25 @@ pub struct AvailableCodeAction {
 }
 
 #[derive(Clone)]
-pub struct CodeActionContents {
+pub(crate) struct CodeActionContents {
     tasks: Option<Rc<ResolvedTasks>>,
     actions: Option<Rc<[AvailableCodeAction]>>,
+    debug_scenarios: Vec<DebugScenario>,
+    pub(crate) context: TaskContext,
 }
 
 impl CodeActionContents {
-    pub fn new(
-        mut tasks: Option<ResolvedTasks>,
+    pub(crate) fn new(
+        tasks: Option<ResolvedTasks>,
         actions: Option<Rc<[AvailableCodeAction]>>,
-        cx: &App,
+        debug_scenarios: Vec<DebugScenario>,
+        context: TaskContext,
     ) -> Self {
-        if !cx.has_flag::<DebuggerFeatureFlag>() {
-            if let Some(tasks) = &mut tasks {
-                tasks
-                    .templates
-                    .retain(|(_, task)| !matches!(task.task_type(), task::TaskType::Debug(_)));
-            }
-        }
-
         Self {
             tasks: tasks.map(Rc::new),
             actions,
+            debug_scenarios,
+            context,
         }
     }
 
@@ -849,21 +848,13 @@ impl CodeActionContents {
     }
 
     fn len(&self) -> usize {
-        match (&self.tasks, &self.actions) {
-            (Some(tasks), Some(actions)) => actions.len() + tasks.templates.len(),
-            (Some(tasks), None) => tasks.templates.len(),
-            (None, Some(actions)) => actions.len(),
-            (None, None) => 0,
-        }
+        let tasks_len = self.tasks.as_ref().map_or(0, |tasks| tasks.templates.len());
+        let code_actions_len = self.actions.as_ref().map_or(0, |actions| actions.len());
+        tasks_len + code_actions_len + self.debug_scenarios.len()
     }
 
     fn is_empty(&self) -> bool {
-        match (&self.tasks, &self.actions) {
-            (Some(tasks), Some(actions)) => actions.is_empty() && tasks.templates.is_empty(),
-            (Some(tasks), None) => tasks.templates.is_empty(),
-            (None, Some(actions)) => actions.is_empty(),
-            (None, None) => true,
-        }
+        self.len() == 0
     }
 
     fn iter(&self) -> impl Iterator<Item = CodeActionsItem> + '_ {
@@ -882,43 +873,38 @@ impl CodeActionContents {
                     provider: available.provider.clone(),
                 })
             }))
+            .chain(
+                self.debug_scenarios
+                    .iter()
+                    .cloned()
+                    .map(CodeActionsItem::DebugScenario),
+            )
     }
 
-    pub fn get(&self, index: usize) -> Option<CodeActionsItem> {
-        match (&self.tasks, &self.actions) {
-            (Some(tasks), Some(actions)) => {
-                if index < tasks.templates.len() {
-                    tasks
-                        .templates
-                        .get(index)
-                        .cloned()
-                        .map(|(kind, task)| CodeActionsItem::Task(kind, task))
-                } else {
-                    actions.get(index - tasks.templates.len()).map(|available| {
-                        CodeActionsItem::CodeAction {
-                            excerpt_id: available.excerpt_id,
-                            action: available.action.clone(),
-                            provider: available.provider.clone(),
-                        }
-                    })
-                }
+    pub fn get(&self, mut index: usize) -> Option<CodeActionsItem> {
+        if let Some(tasks) = &self.tasks {
+            if let Some((kind, task)) = tasks.templates.get(index) {
+                return Some(CodeActionsItem::Task(kind.clone(), task.clone()));
+            } else {
+                index -= tasks.templates.len();
             }
-            (Some(tasks), None) => tasks
-                .templates
-                .get(index)
-                .cloned()
-                .map(|(kind, task)| CodeActionsItem::Task(kind, task)),
-            (None, Some(actions)) => {
-                actions
-                    .get(index)
-                    .map(|available| CodeActionsItem::CodeAction {
-                        excerpt_id: available.excerpt_id,
-                        action: available.action.clone(),
-                        provider: available.provider.clone(),
-                    })
+        }
+        if let Some(actions) = &self.actions {
+            if let Some(available) = actions.get(index) {
+                return Some(CodeActionsItem::CodeAction {
+                    excerpt_id: available.excerpt_id,
+                    action: available.action.clone(),
+                    provider: available.provider.clone(),
+                });
+            } else {
+                index -= actions.len();
             }
-            (None, None) => None,
         }
+
+        self.debug_scenarios
+            .get(index)
+            .cloned()
+            .map(CodeActionsItem::DebugScenario)
     }
 }
 
@@ -931,6 +917,7 @@ pub enum CodeActionsItem {
         action: CodeAction,
         provider: Rc<dyn CodeActionProvider>,
     },
+    DebugScenario(DebugScenario),
 }
 
 impl CodeActionsItem {
@@ -947,16 +934,23 @@ impl CodeActionsItem {
         };
         Some(action)
     }
+    fn as_debug_scenario(&self) -> Option<&DebugScenario> {
+        let Self::DebugScenario(scenario) = self else {
+            return None;
+        };
+        Some(scenario)
+    }
 
     pub fn label(&self) -> String {
         match self {
             Self::CodeAction { action, .. } => action.lsp_action.title().to_owned(),
             Self::Task(_, task) => task.resolved_label.clone(),
+            Self::DebugScenario(scenario) => scenario.label.to_string(),
         }
     }
 }
 
-pub struct CodeActionsMenu {
+pub(crate) struct CodeActionsMenu {
     pub actions: CodeActionContents,
     pub buffer: Entity<Buffer>,
     pub selected_item: usize,
@@ -1065,19 +1059,7 @@ impl CodeActionsMenu {
                                 .inset(true)
                                 .toggle_state(selected)
                                 .when_some(action.as_code_action(), |this, action| {
-                                    this.on_click(cx.listener(move |editor, _, window, cx| {
-                                        cx.stop_propagation();
-                                        if let Some(task) = editor.confirm_code_action(
-                                            &ConfirmCodeAction {
-                                                item_ix: Some(item_ix),
-                                            },
-                                            window,
-                                            cx,
-                                        ) {
-                                            task.detach_and_log_err(cx)
-                                        }
-                                    }))
-                                    .child(
+                                    this.child(
                                         h_flex()
                                             .overflow_hidden()
                                             .child(
@@ -1090,19 +1072,7 @@ impl CodeActionsMenu {
                                     )
                                 })
                                 .when_some(action.as_task(), |this, task| {
-                                    this.on_click(cx.listener(move |editor, _, window, cx| {
-                                        cx.stop_propagation();
-                                        if let Some(task) = editor.confirm_code_action(
-                                            &ConfirmCodeAction {
-                                                item_ix: Some(item_ix),
-                                            },
-                                            window,
-                                            cx,
-                                        ) {
-                                            task.detach_and_log_err(cx)
-                                        }
-                                    }))
-                                    .child(
+                                    this.child(
                                         h_flex()
                                             .overflow_hidden()
                                             .child(task.resolved_label.replace("\n", ""))
@@ -1110,7 +1080,29 @@ impl CodeActionsMenu {
                                                 this.text_color(colors.text_accent)
                                             }),
                                     )
-                                }),
+                                })
+                                .when_some(action.as_debug_scenario(), |this, scenario| {
+                                    this.child(
+                                        h_flex()
+                                            .overflow_hidden()
+                                            .child(scenario.label.clone())
+                                            .when(selected, |this| {
+                                                this.text_color(colors.text_accent)
+                                            }),
+                                    )
+                                })
+                                .on_click(cx.listener(move |editor, _, window, cx| {
+                                    cx.stop_propagation();
+                                    if let Some(task) = editor.confirm_code_action(
+                                        &ConfirmCodeAction {
+                                            item_ix: Some(item_ix),
+                                        },
+                                        window,
+                                        cx,
+                                    ) {
+                                        task.detach_and_log_err(cx)
+                                    }
+                                })),
                         )
                     })
                     .collect()
@@ -1128,6 +1120,7 @@ impl CodeActionsMenu {
                     CodeActionsItem::CodeAction { action, .. } => {
                         action.lsp_action.title().chars().count()
                     }
+                    CodeActionsItem::DebugScenario(scenario) => scenario.label.chars().count(),
                 })
                 .map(|(ix, _)| ix),
         )

crates/editor/src/editor.rs 🔗

@@ -5089,6 +5089,7 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let quick_launch = action.quick_launch;
         let mut context_menu = self.context_menu.borrow_mut();
         if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
             if code_actions.deployed_from_indicator == action.deployed_from_indicator {
@@ -5162,8 +5163,6 @@ impl Editor {
                                 Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
                             });
 
-                    let debugger_flag = cx.has_flag::<DebuggerFeatureFlag>();
-
                     Some(cx.spawn_in(window, async move |editor, cx| {
                         let task_context = match task_context {
                             Some(task_context) => task_context.await,
@@ -5171,7 +5170,7 @@ impl Editor {
                         };
                         let resolved_tasks =
                             tasks
-                                .zip(task_context)
+                                .zip(task_context.clone())
                                 .map(|(tasks, task_context)| ResolvedTasks {
                                     templates: tasks.resolve(&task_context).collect(),
                                     position: snapshot.buffer_snapshot.anchor_before(Point::new(
@@ -5179,22 +5178,49 @@ impl Editor {
                                         tasks.column,
                                     )),
                                 });
-                        let spawn_straight_away = resolved_tasks.as_ref().map_or(false, |tasks| {
-                            tasks
-                                .templates
-                                .iter()
-                                .filter(|task| {
-                                    if matches!(task.1.task_type(), task::TaskType::Debug(_)) {
-                                        debugger_flag
-                                    } else {
-                                        true
-                                    }
+                        let spawn_straight_away = quick_launch
+                            && resolved_tasks
+                                .as_ref()
+                                .map_or(false, |tasks| tasks.templates.len() == 1)
+                            && code_actions
+                                .as_ref()
+                                .map_or(true, |actions| actions.is_empty());
+                        let debug_scenarios = editor.update(cx, |editor, cx| {
+                            if cx.has_flag::<DebuggerFeatureFlag>() {
+                                maybe!({
+                                    let project = editor.project.as_ref()?;
+                                    let dap_store = project.read(cx).dap_store();
+                                    let mut scenarios = vec![];
+                                    let resolved_tasks = resolved_tasks.as_ref()?;
+                                    let debug_adapter: SharedString = buffer
+                                        .read(cx)
+                                        .language()?
+                                        .context_provider()?
+                                        .debug_adapter()?
+                                        .into();
+                                    dap_store.update(cx, |this, cx| {
+                                        for (_, task) in &resolved_tasks.templates {
+                                            if let Some(scenario) = this
+                                                .debug_scenario_for_build_task(
+                                                    task.resolved.clone(),
+                                                    SharedString::from(
+                                                        task.original_task().label.clone(),
+                                                    ),
+                                                    debug_adapter.clone(),
+                                                    cx,
+                                                )
+                                            {
+                                                scenarios.push(scenario);
+                                            }
+                                        }
+                                    });
+                                    Some(scenarios)
                                 })
-                                .count()
-                                == 1
-                        }) && code_actions
-                            .as_ref()
-                            .map_or(true, |actions| actions.is_empty());
+                                .unwrap_or_default()
+                            } else {
+                                vec![]
+                            }
+                        })?;
                         if let Ok(task) = editor.update_in(cx, |editor, window, cx| {
                             *editor.context_menu.borrow_mut() =
                                 Some(CodeContextMenu::CodeActions(CodeActionsMenu {
@@ -5202,7 +5228,8 @@ impl Editor {
                                     actions: CodeActionContents::new(
                                         resolved_tasks,
                                         code_actions,
-                                        cx,
+                                        debug_scenarios,
+                                        task_context.unwrap_or_default(),
                                     ),
                                     selected_item: Default::default(),
                                     scroll_handle: UniformListScrollHandle::default(),
@@ -5262,25 +5289,17 @@ impl Editor {
 
         match action {
             CodeActionsItem::Task(task_source_kind, resolved_task) => {
-                match resolved_task.task_type() {
-                    task::TaskType::Script => workspace.update(cx, |workspace, cx| {
-                        workspace.schedule_resolved_task(
-                            task_source_kind,
-                            resolved_task,
-                            false,
-                            window,
-                            cx,
-                        );
+                workspace.update(cx, |workspace, cx| {
+                    workspace.schedule_resolved_task(
+                        task_source_kind,
+                        resolved_task,
+                        false,
+                        window,
+                        cx,
+                    );
 
-                        Some(Task::ready(Ok(())))
-                    }),
-                    task::TaskType::Debug(_) => {
-                        workspace.update(cx, |workspace, cx| {
-                            workspace.schedule_debug_task(resolved_task, window, cx);
-                        });
-                        Some(Task::ready(Ok(())))
-                    }
-                }
+                    Some(Task::ready(Ok(())))
+                })
             }
             CodeActionsItem::CodeAction {
                 excerpt_id,
@@ -5302,6 +5321,14 @@ impl Editor {
                     .await
                 }))
             }
+            CodeActionsItem::DebugScenario(scenario) => {
+                let context = actions_menu.actions.context.clone();
+
+                workspace.update(cx, |workspace, cx| {
+                    workspace.start_debug_session(scenario, context, Some(buffer), window, cx);
+                });
+                Some(Task::ready(Ok(())))
+            }
         }
     }
 
@@ -6660,6 +6687,7 @@ impl Editor {
                                     "Toggle Code Actions",
                                     &ToggleCodeActions {
                                         deployed_from_indicator: None,
+                                        quick_launch: false,
                                     },
                                     &focus_handle,
                                     window,
@@ -6668,11 +6696,13 @@ impl Editor {
                             }
                         })
                     })
-                    .on_click(cx.listener(move |editor, _e, window, cx| {
+                    .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| {
+                        let quick_launch = e.down.button == MouseButton::Left;
                         window.focus(&editor.focus_handle(cx));
                         editor.toggle_code_actions(
                             &ToggleCodeActions {
                                 deployed_from_indicator: Some(row),
+                                quick_launch,
                             },
                             window,
                             cx,
@@ -7050,7 +7080,7 @@ impl Editor {
             let context = task_context.await?;
             let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?;
 
-            let resolved = resolved_task.resolved.as_mut()?;
+            let resolved = &mut resolved_task.resolved;
             resolved.reveal = reveal_strategy;
 
             workspace
@@ -7140,11 +7170,13 @@ impl Editor {
             .icon_size(IconSize::XSmall)
             .icon_color(color)
             .toggle_state(is_active)
-            .on_click(cx.listener(move |editor, _e, window, cx| {
+            .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| {
+                let quick_launch = e.down.button == MouseButton::Left;
                 window.focus(&editor.focus_handle(cx));
                 editor.toggle_code_actions(
                     &ToggleCodeActions {
                         deployed_from_indicator: Some(row),
+                        quick_launch,
                     },
                     window,
                     cx,

crates/editor/src/mouse_context_menu.rs 🔗

@@ -211,6 +211,7 @@ pub fn deploy_context_menu(
                     "Show Code Actions",
                     Box::new(ToggleCodeActions {
                         deployed_from_indicator: None,
+                        quick_launch: false,
                     }),
                 )
                 .separator()

crates/language/src/task_context.rs 🔗

@@ -47,4 +47,7 @@ pub trait ContextProvider: Send + Sync {
     fn lsp_task_source(&self) -> Option<LanguageServerName> {
         None
     }
+
+    /// Default debug adapter for a given language.
+    fn debug_adapter(&self) -> Option<String>;
 }

crates/languages/src/go.rs 🔗

@@ -630,6 +630,10 @@ impl ContextProvider for GoContextProvider {
             },
         ]))
     }
+
+    fn debug_adapter(&self) -> Option<String> {
+        Some("Delve".into())
+    }
 }
 
 fn extract_subtest_name(input: &str) -> Option<String> {

crates/languages/src/python.rs 🔗

@@ -503,6 +503,10 @@ impl ContextProvider for PythonContextProvider {
 
         Some(TaskTemplates(tasks))
     }
+
+    fn debug_adapter(&self) -> Option<String> {
+        Some("Debugpy".into())
+    }
 }
 
 fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner {

crates/languages/src/rust.rs 🔗

@@ -20,7 +20,7 @@ use std::{
     path::{Path, PathBuf},
     sync::{Arc, LazyLock},
 };
-use task::{TaskTemplate, TaskTemplates, TaskType, TaskVariables, VariableName};
+use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
 use util::merge_json_value_into;
 use util::{ResultExt, fs::remove_matching, maybe};
 
@@ -629,7 +629,7 @@ impl ContextProvider for RustContextProvider {
         } else {
             vec!["run".into()]
         };
-        let debug_task_args = if let Some(package_to_run) = package_to_run {
+        let build_task_args = if let Some(package_to_run) = package_to_run {
             vec!["build".into(), "-p".into(), package_to_run]
         } else {
             vec!["build".into()]
@@ -675,32 +675,6 @@ 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: "CodeLLDB".to_owned(),
-                    request: task::DebugArgsRequest::Launch,
-                    locator: Some("cargo".into()),
-                    tcp_connection: None,
-                    initialize_args: None,
-                    stop_on_entry: 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: {})",
@@ -780,31 +754,41 @@ impl ContextProvider for RustContextProvider {
                 cwd: Some("$ZED_DIRNAME".to_owned()),
                 ..TaskTemplate::default()
             },
+            TaskTemplate {
+                label: "Clean".into(),
+                command: "cargo".into(),
+                args: vec!["clean".into()],
+                cwd: Some("$ZED_DIRNAME".to_owned()),
+                ..TaskTemplate::default()
+            },
             TaskTemplate {
                 label: format!(
-                    "Debug {} {} (package: {})",
+                    "Build {} {} (package: {})",
                     RUST_BIN_KIND_TASK_VARIABLE.template_value(),
                     RUST_BIN_NAME_TASK_VARIABLE.template_value(),
                     RUST_PACKAGE_TASK_VARIABLE.template_value(),
                 ),
                 cwd: Some("$ZED_DIRNAME".to_owned()),
                 command: "cargo".into(),
-                task_type: TaskType::Debug(task::DebugArgs {
-                    request: task::DebugArgsRequest::Launch,
-                    adapter: "CodeLLDB".to_owned(),
-                    initialize_args: None,
-                    locator: Some("cargo".into()),
-                    tcp_connection: None,
-                    stop_on_entry: None,
-                }),
-                args: debug_task_args,
+                args: build_task_args,
                 tags: vec!["rust-main".to_owned()],
                 ..TaskTemplate::default()
             },
             TaskTemplate {
-                label: "Clean".into(),
+                label: format!(
+                    "Build Test '{}' (package: {})",
+                    RUST_TEST_NAME_TASK_VARIABLE.template_value(),
+                    RUST_PACKAGE_TASK_VARIABLE.template_value(),
+                ),
                 command: "cargo".into(),
-                args: vec!["clean".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()
             },
@@ -832,6 +816,10 @@ impl ContextProvider for RustContextProvider {
     fn lsp_task_source(&self) -> Option<LanguageServerName> {
         Some(SERVER_NAME)
     }
+
+    fn debug_adapter(&self) -> Option<String> {
+        Some("CodeLLDB".to_owned())
+    }
 }
 
 /// Part of the data structure of Cargo metadata

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

@@ -1,6 +1,6 @@
 use super::{
     breakpoint_store::BreakpointStore,
-    locators::DapLocator,
+    locators,
     session::{self, Session, SessionStateEvent},
 };
 use crate::{
@@ -13,10 +13,12 @@ use anyhow::{Result, anyhow};
 use async_trait::async_trait;
 use collections::HashMap;
 use dap::{
-    Capabilities, CompletionItem, CompletionsArguments, DapRegistry, EvaluateArguments,
-    EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments, Source,
-    StackFrameId, StartDebuggingRequestArguments,
-    adapters::{DapStatus, DebugAdapterBinary, DebugAdapterName, TcpArguments},
+    Capabilities, CompletionItem, CompletionsArguments, DapRegistry, DebugRequest,
+    EvaluateArguments, EvaluateArgumentsContext, EvaluateResponse, RunInTerminalRequestArguments,
+    Source, StackFrameId, StartDebuggingRequestArguments,
+    adapters::{
+        DapStatus, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments,
+    },
     client::SessionId,
     messages::Message,
     requests::{Completions, Evaluate, Request as _, RunInTerminal, StartDebugging},
@@ -49,9 +51,9 @@ use std::{
     ffi::OsStr,
     net::Ipv4Addr,
     path::{Path, PathBuf},
-    sync::Arc,
+    sync::{Arc, Once},
 };
-use task::{DebugTaskDefinition, DebugTaskTemplate};
+use task::{DebugScenario, SpawnInTerminal};
 use util::ResultExt as _;
 use worktree::Worktree;
 
@@ -95,7 +97,6 @@ pub struct LocalDapStore {
     environment: Entity<ProjectEnvironment>,
     language_registry: Arc<LanguageRegistry>,
     toolchain_store: Arc<dyn LanguageToolchainStore>,
-    locators: HashMap<String, Arc<dyn DapLocator>>,
 }
 
 pub struct SshDapStore {
@@ -118,9 +119,14 @@ pub struct DapStore {
 impl EventEmitter<DapStoreEvent> for DapStore {}
 
 impl DapStore {
-    pub fn init(client: &AnyProtoClient) {
+    pub fn init(client: &AnyProtoClient, cx: &mut App) {
+        static ADD_LOCATORS: Once = Once::new();
         client.add_entity_request_handler(Self::handle_run_debug_locator);
         client.add_entity_request_handler(Self::handle_get_debug_adapter_binary);
+        ADD_LOCATORS.call_once(|| {
+            DapRegistry::global(cx)
+                .add_locator("cargo".into(), Arc::new(locators::cargo::CargoLocator {}))
+        });
     }
 
     #[expect(clippy::too_many_arguments)]
@@ -135,11 +141,6 @@ impl DapStore {
         breakpoint_store: Entity<BreakpointStore>,
         cx: &mut Context<Self>,
     ) -> Self {
-        let locators = HashMap::from_iter([(
-            "cargo".to_string(),
-            Arc::new(super::locators::cargo::CargoLocator {}) as _,
-        )]);
-
         let mode = DapStoreMode::Local(LocalDapStore {
             fs,
             environment,
@@ -147,7 +148,6 @@ impl DapStore {
             node_runtime,
             toolchain_store,
             language_registry,
-            locators,
         });
 
         Self::new(mode, breakpoint_store, worktree_store, cx)
@@ -273,7 +273,7 @@ impl DapStore {
             DapStoreMode::Ssh(ssh) => {
                 let request = ssh.upstream_client.request(proto::GetDebugAdapterBinary {
                     project_id: ssh.upstream_project_id,
-                    task: Some(definition.to_proto()),
+                    definition: Some(definition.to_proto()),
                 });
                 let ssh_client = ssh.ssh_client.clone();
 
@@ -326,34 +326,100 @@ impl DapStore {
         }
     }
 
+    pub fn debug_scenario_for_build_task(
+        &self,
+        mut build: SpawnInTerminal,
+        unresoved_label: SharedString,
+        adapter: SharedString,
+        cx: &mut App,
+    ) -> Option<DebugScenario> {
+        build.args = build
+            .args
+            .into_iter()
+            .map(|arg| {
+                if arg.starts_with("$") {
+                    arg.strip_prefix("$")
+                        .and_then(|arg| build.env.get(arg).map(ToOwned::to_owned))
+                        .unwrap_or_else(|| arg)
+                } else {
+                    arg
+                }
+            })
+            .collect();
+
+        DapRegistry::global(cx)
+            .locators()
+            .values()
+            .find(|locator| locator.accepts(&build))
+            .map(|_| DebugScenario {
+                adapter,
+                label: format!("Debug `{}`", build.label).into(),
+                build: Some(unresoved_label),
+                request: None,
+                initialize_args: None,
+                tcp_connection: None,
+                stop_on_entry: None,
+            })
+    }
+
     pub fn run_debug_locator(
         &mut self,
-        template: DebugTaskTemplate,
+        mut build_command: SpawnInTerminal,
         cx: &mut Context<Self>,
-    ) -> Task<Result<DebugTaskDefinition>> {
-        let Some(locator_name) = template.locator else {
-            return Task::ready(Ok(template.definition));
-        };
-
+    ) -> Task<Result<DebugRequest>> {
         match &self.mode {
-            DapStoreMode::Local(local) => {
-                if let Some(locator) = local.locators.get(&locator_name).cloned() {
-                    cx.background_spawn(
-                        async move { locator.run_locator(template.definition).await },
-                    )
+            DapStoreMode::Local(_) => {
+                // Pre-resolve args with existing environment.
+                build_command.args = build_command
+                    .args
+                    .into_iter()
+                    .map(|arg| {
+                        if arg.starts_with("$") {
+                            arg.strip_prefix("$")
+                                .and_then(|arg| build_command.env.get(arg).map(ToOwned::to_owned))
+                                .unwrap_or_else(|| arg)
+                        } else {
+                            arg
+                        }
+                    })
+                    .collect();
+                let locators = DapRegistry::global(cx)
+                    .locators()
+                    .values()
+                    .filter(|locator| locator.accepts(&build_command))
+                    .cloned()
+                    .collect::<Vec<_>>();
+                if !locators.is_empty() {
+                    cx.background_spawn(async move {
+                        for locator in locators {
+                            let result = locator
+                                .run(build_command.clone())
+                                .await
+                                .log_with_level(log::Level::Error);
+                            if let Some(result) = result {
+                                return Ok(result);
+                            }
+                        }
+                        Err(anyhow!(
+                            "None of the locators for task `{}` completed successfully",
+                            build_command.label
+                        ))
+                    })
                 } else {
-                    Task::ready(Err(anyhow!("Couldn't find locator {}", locator_name)))
+                    Task::ready(Err(anyhow!(
+                        "Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition",
+                        build_command.label
+                    )))
                 }
             }
             DapStoreMode::Ssh(ssh) => {
-                let request = ssh.upstream_client.request(proto::RunDebugLocator {
+                let request = ssh.upstream_client.request(proto::RunDebugLocators {
                     project_id: ssh.upstream_project_id,
-                    locator: locator_name,
-                    task: Some(template.definition.to_proto()),
+                    build_command: Some(build_command.to_proto()),
                 });
                 cx.background_spawn(async move {
                     let response = request.await?;
-                    DebugTaskDefinition::from_proto(response)
+                    DebugRequest::from_proto(response)
                 })
             }
             DapStoreMode::Collab => {
@@ -943,22 +1009,19 @@ impl DapStore {
 
     async fn handle_run_debug_locator(
         this: Entity<Self>,
-        envelope: TypedEnvelope<proto::RunDebugLocator>,
+        envelope: TypedEnvelope<proto::RunDebugLocators>,
         mut cx: AsyncApp,
-    ) -> Result<proto::DebugTaskDefinition> {
-        let template = DebugTaskTemplate {
-            locator: Some(envelope.payload.locator),
-            definition: DebugTaskDefinition::from_proto(
-                envelope
-                    .payload
-                    .task
-                    .ok_or_else(|| anyhow!("missing definition"))?,
-            )?,
-        };
-        let definition = this
-            .update(&mut cx, |this, cx| this.run_debug_locator(template, cx))?
+    ) -> Result<proto::DebugRequest> {
+        let task = envelope
+            .payload
+            .build_command
+            .ok_or_else(|| anyhow!("missing definition"))?;
+        let build_task = SpawnInTerminal::from_proto(task);
+        let request = this
+            .update(&mut cx, |this, cx| this.run_debug_locator(build_task, cx))?
             .await?;
-        Ok(definition.to_proto())
+
+        Ok(request.to_proto())
     }
 
     async fn handle_get_debug_adapter_binary(
@@ -969,7 +1032,7 @@ impl DapStore {
         let definition = DebugTaskDefinition::from_proto(
             envelope
                 .payload
-                .task
+                .definition
                 .ok_or_else(|| anyhow!("missing definition"))?,
         )?;
         let binary = this

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

@@ -1,34 +0,0 @@
-use anyhow::{Result, anyhow};
-use cargo::CargoLocator;
-use collections::HashMap;
-use gpui::SharedString;
-use locators::DapLocator;
-use task::{DebugTaskDefinition, DebugTaskTemplate};
-
-mod cargo;
-pub mod locators;
-
-pub(super) struct LocatorStore {
-    locators: HashMap<SharedString, Box<dyn DapLocator>>,
-}
-
-impl LocatorStore {
-    pub(super) fn new() -> Self {
-        Self { locators }
-    }
-
-    pub(super) async fn resolve_debug_config(
-        &self,
-        template: DebugTaskTemplate,
-    ) -> Result<DebugTaskDefinition> {
-        let Some(locator_name) = &template.locator else {
-            return Ok(template.definition);
-        };
-
-        if let Some(locator) = self.locators.get(locator_name as &str) {
-            locator.run_locator(template.definition).await
-        } else {
-            Err(anyhow!("Couldn't find locator {}", locator_name))
-        }
-    }
-}

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

@@ -1,10 +1 @@
-use anyhow::Result;
-use async_trait::async_trait;
-use task::DebugTaskDefinition;
-
 pub(crate) mod cargo;
-
-#[async_trait]
-pub(super) trait DapLocator: Send + Sync {
-    async fn run_locator(&self, debug_config: DebugTaskDefinition) -> Result<DebugTaskDefinition>;
-}

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

@@ -1,12 +1,12 @@
-use super::DapLocator;
 use anyhow::{Result, anyhow};
 use async_trait::async_trait;
+use dap::{DapLocator, DebugRequest};
 use serde_json::Value;
 use smol::{
     io::AsyncReadExt,
     process::{Command, Stdio},
 };
-use task::DebugTaskDefinition;
+use task::SpawnInTerminal;
 
 pub(crate) struct CargoLocator;
 
@@ -37,26 +37,31 @@ async fn find_best_executable(executables: &[String], test_name: &str) -> Option
 }
 #[async_trait]
 impl DapLocator for CargoLocator {
-    async fn run_locator(
-        &self,
-        mut debug_config: DebugTaskDefinition,
-    ) -> Result<DebugTaskDefinition> {
-        let Some(launch_config) = (match &mut debug_config.request {
-            task::DebugRequest::Launch(launch_config) => Some(launch_config),
-            _ => None,
-        }) else {
-            return Err(anyhow!("Couldn't get launch config in locator"));
+    fn accepts(&self, build_config: &SpawnInTerminal) -> bool {
+        if build_config.command != "cargo" {
+            return false;
+        }
+        let Some(command) = build_config.args.first().map(|s| s.as_str()) else {
+            return false;
         };
+        if matches!(command, "check" | "run") {
+            return false;
+        }
+        !matches!(command, "test" | "bench")
+            || build_config.args.iter().any(|arg| arg == "--no-run")
+    }
 
-        let Some(cwd) = launch_config.cwd.clone() else {
+    async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest> {
+        let Some(cwd) = build_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(&launch_config.args)
+            .args(&build_config.args)
             .arg("--message-format=json")
+            .envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone())))
             .current_dir(cwd)
             .stdout(Stdio::piped())
             .spawn()?;
@@ -85,19 +90,16 @@ impl DapLocator for CargoLocator {
             return Err(anyhow!("Couldn't get executable in cargo locator"));
         };
 
-        let is_test = launch_config
-            .args
-            .first()
-            .map_or(false, |arg| arg == "test");
+        let is_test = build_config.args.first().map_or(false, |arg| arg == "test");
 
         let mut test_name = None;
         if is_test {
-            if let Some(package_index) = launch_config
+            if let Some(package_index) = build_config
                 .args
                 .iter()
                 .position(|arg| arg == "-p" || arg == "--package")
             {
-                test_name = launch_config
+                test_name = build_config
                     .args
                     .get(package_index + 2)
                     .filter(|name| !name.starts_with("--"))
@@ -116,12 +118,17 @@ impl DapLocator for CargoLocator {
             return Err(anyhow!("Couldn't get executable in cargo locator"));
         };
 
-        launch_config.program = executable;
+        let args = test_name.into_iter().collect();
 
-        launch_config.args.clear();
-        if let Some(test_name) = test_name {
-            launch_config.args.push(test_name);
-        }
-        Ok(debug_config)
+        Ok(DebugRequest::Launch(task::LaunchRequest {
+            program: executable,
+            cwd: build_config.cwd.clone(),
+            args,
+            env: build_config
+                .env
+                .iter()
+                .map(|(k, v)| (k.clone(), v.clone()))
+                .collect(),
+        }))
     }
 }

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

@@ -12,7 +12,7 @@ use super::dap_command::{
 use super::dap_store::DapStore;
 use anyhow::{Context as _, Result, anyhow};
 use collections::{HashMap, HashSet, IndexMap, IndexSet};
-use dap::adapters::DebugAdapterBinary;
+use dap::adapters::{DebugAdapterBinary, DebugTaskDefinition};
 use dap::messages::Response;
 use dap::{
     Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId,
@@ -42,7 +42,6 @@ use std::{
     path::Path,
     sync::Arc,
 };
-use task::DebugTaskDefinition;
 use text::{PointUtf16, ToPointUtf16};
 use util::{ResultExt, merge_json_value_into};
 use worktree::Worktree;
@@ -125,7 +124,6 @@ enum Mode {
 pub struct LocalMode {
     client: Arc<DebugAdapterClient>,
     binary: DebugAdapterBinary,
-    root_binary: Option<Arc<DebugAdapterBinary>>,
     pub(crate) breakpoint_store: Entity<BreakpointStore>,
     tmp_breakpoint: Option<SourceBreakpoint>,
     worktree: WeakEntity<Worktree>,
@@ -160,12 +158,6 @@ impl LocalMode {
             messages_tx.unbounded_send(message).ok();
         });
 
-        let root_binary = if let Some(parent_session) = parent_session.as_ref() {
-            Some(parent_session.read_with(&cx, |session, _| session.root_binary().clone())?)
-        } else {
-            None
-        };
-
         let client = Arc::new(
             if let Some(client) = parent_session
                 .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok())
@@ -186,7 +178,6 @@ impl LocalMode {
             breakpoint_store,
             worktree,
             tmp_breakpoint: None,
-            root_binary,
             binary,
         })
     }
@@ -834,19 +825,6 @@ impl Session {
         &self.capabilities
     }
 
-    pub(crate) fn root_binary(&self) -> Arc<DebugAdapterBinary> {
-        match &self.mode {
-            Mode::Building => {
-                // todo(debugger): Implement root_binary for building mode
-                unimplemented!()
-            }
-            Mode::Running(running) => running
-                .root_binary
-                .clone()
-                .unwrap_or_else(|| Arc::new(running.binary.clone())),
-        }
-    }
-
     pub fn binary(&self) -> &DebugAdapterBinary {
         let Mode::Running(local_mode) = &self.mode else {
             panic!("Session is not local");
@@ -855,10 +833,10 @@ impl Session {
     }
 
     pub fn adapter_name(&self) -> SharedString {
-        self.definition.adapter.clone().into()
+        self.definition.adapter.clone()
     }
 
-    pub fn label(&self) -> String {
+    pub fn label(&self) -> SharedString {
         self.definition.label.clone()
     }
 
@@ -889,7 +867,7 @@ impl Session {
     }
 
     pub(super) fn request_initialize(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let adapter_id = self.definition.adapter.clone();
+        let adapter_id = String::from(self.definition.adapter.clone());
         let request = Initialize { adapter_id };
         match &self.mode {
             Mode::Running(local_mode) => {

crates/project/src/project.rs 🔗

@@ -826,7 +826,7 @@ impl Project {
         SettingsObserver::init(&client);
         TaskStore::init(Some(&client));
         ToolchainStore::init(&client);
-        DapStore::init(&client);
+        DapStore::init(&client, cx);
         BreakpointStore::init(&client);
     }
 
@@ -1159,7 +1159,7 @@ impl Project {
             SettingsObserver::init(&ssh_proto);
             TaskStore::init(Some(&ssh_proto));
             ToolchainStore::init(&ssh_proto);
-            DapStore::init(&ssh_proto);
+            DapStore::init(&ssh_proto, cx);
             GitStore::init(&ssh_proto);
 
             this

crates/project/src/project_settings.rs 🔗

@@ -8,7 +8,7 @@ use lsp::LanguageServerName;
 use paths::{
     EDITORCONFIG_NAME, local_debug_file_relative_path, local_settings_file_relative_path,
     local_tasks_file_relative_path, local_vscode_launch_file_relative_path,
-    local_vscode_tasks_file_relative_path,
+    local_vscode_tasks_file_relative_path, task_file_name,
 };
 use rpc::{
     AnyProtoClient, TypedEnvelope,
@@ -18,7 +18,7 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{
     InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
-    SettingsStore, TaskKind, parse_json_with_comments, watch_config_file,
+    SettingsStore, parse_json_with_comments, watch_config_file,
 };
 use std::{
     path::{Path, PathBuf},
@@ -377,7 +377,7 @@ pub struct SettingsObserver {
     worktree_store: Entity<WorktreeStore>,
     project_id: u64,
     task_store: Entity<TaskStore>,
-    _global_task_config_watchers: (Task<()>, Task<()>),
+    _global_task_config_watcher: Task<()>,
 }
 
 /// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
@@ -405,19 +405,10 @@ impl SettingsObserver {
             mode: SettingsObserverMode::Local(fs.clone()),
             downstream_client: None,
             project_id: 0,
-            _global_task_config_watchers: (
-                Self::subscribe_to_global_task_file_changes(
-                    fs.clone(),
-                    TaskKind::Script,
-                    paths::tasks_file().clone(),
-                    cx,
-                ),
-                Self::subscribe_to_global_task_file_changes(
-                    fs,
-                    TaskKind::Debug,
-                    paths::debug_tasks_file().clone(),
-                    cx,
-                ),
+            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
+                fs.clone(),
+                paths::tasks_file().clone(),
+                cx,
             ),
         }
     }
@@ -434,19 +425,10 @@ impl SettingsObserver {
             mode: SettingsObserverMode::Remote,
             downstream_client: None,
             project_id: 0,
-            _global_task_config_watchers: (
-                Self::subscribe_to_global_task_file_changes(
-                    fs.clone(),
-                    TaskKind::Script,
-                    paths::tasks_file().clone(),
-                    cx,
-                ),
-                Self::subscribe_to_global_task_file_changes(
-                    fs.clone(),
-                    TaskKind::Debug,
-                    paths::debug_tasks_file().clone(),
-                    cx,
-                ),
+            _global_task_config_watcher: Self::subscribe_to_global_task_file_changes(
+                fs.clone(),
+                paths::tasks_file().clone(),
+                cx,
             ),
         }
     }
@@ -575,7 +557,7 @@ impl SettingsObserver {
                         )
                         .unwrap(),
                 );
-                (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
+                (settings_dir, LocalSettingsKind::Tasks)
             } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
                 let settings_dir = Arc::<Path>::from(
                     path.ancestors()
@@ -587,7 +569,7 @@ impl SettingsObserver {
                         )
                         .unwrap(),
                 );
-                (settings_dir, LocalSettingsKind::Tasks(TaskKind::Script))
+                (settings_dir, LocalSettingsKind::Tasks)
             } else if path.ends_with(local_debug_file_relative_path()) {
                 let settings_dir = Arc::<Path>::from(
                     path.ancestors()
@@ -599,7 +581,7 @@ impl SettingsObserver {
                         )
                         .unwrap(),
                 );
-                (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
+                (settings_dir, LocalSettingsKind::Debug)
             } else if path.ends_with(local_vscode_launch_file_relative_path()) {
                 let settings_dir = Arc::<Path>::from(
                     path.ancestors()
@@ -611,7 +593,7 @@ impl SettingsObserver {
                         )
                         .unwrap(),
                 );
-                (settings_dir, LocalSettingsKind::Tasks(TaskKind::Debug))
+                (settings_dir, LocalSettingsKind::Debug)
             } else if path.ends_with(EDITORCONFIG_NAME) {
                 let Some(settings_dir) = path.parent().map(Arc::from) else {
                     continue;
@@ -747,7 +729,7 @@ impl SettingsObserver {
                             }
                         }
                     }),
-                LocalSettingsKind::Tasks(task_kind) => {
+                LocalSettingsKind::Tasks => {
                     let result = task_store.update(cx, |task_store, cx| {
                         task_store.update_user_tasks(
                             TaskSettingsLocation::Worktree(SettingsLocation {
@@ -755,7 +737,6 @@ impl SettingsObserver {
                                 path: directory.as_ref(),
                             }),
                             file_content.as_deref(),
-                            task_kind,
                             cx,
                         )
                     });
@@ -772,7 +753,38 @@ impl SettingsObserver {
                         }
                         Ok(()) => {
                             cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
-                                task_kind.config_in_dir(&directory)
+                                directory.join(task_file_name())
+                            )));
+                        }
+                    }
+                }
+                LocalSettingsKind::Debug => {
+                    let result = task_store.update(cx, |task_store, cx| {
+                        task_store.update_user_debug_scenarios(
+                            TaskSettingsLocation::Worktree(SettingsLocation {
+                                worktree_id,
+                                path: directory.as_ref(),
+                            }),
+                            file_content.as_deref(),
+                            cx,
+                        )
+                    });
+
+                    match result {
+                        Err(InvalidSettingsError::Debug { path, message }) => {
+                            log::error!(
+                                "Failed to set local debug scenarios in {path:?}: {message:?}"
+                            );
+                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Err(
+                                InvalidSettingsError::Debug { path, message },
+                            )));
+                        }
+                        Err(e) => {
+                            log::error!("Failed to set local tasks: {e}");
+                        }
+                        Ok(()) => {
+                            cx.emit(SettingsObserverEvent::LocalTasksUpdated(Ok(
+                                directory.join(task_file_name())
                             )));
                         }
                     }
@@ -795,7 +807,6 @@ impl SettingsObserver {
 
     fn subscribe_to_global_task_file_changes(
         fs: Arc<dyn Fs>,
-        task_kind: TaskKind,
         file_path: PathBuf,
         cx: &mut Context<Self>,
     ) -> Task<()> {
@@ -815,7 +826,6 @@ impl SettingsObserver {
                         .update_user_tasks(
                             TaskSettingsLocation::Global(&file_path),
                             Some(&user_tasks_content),
-                            task_kind,
                             cx,
                         )
                         .log_err();
@@ -828,7 +838,6 @@ impl SettingsObserver {
                     task_store.update_user_tasks(
                         TaskSettingsLocation::Global(&file_path),
                         Some(&user_tasks_content),
-                        task_kind,
                         cx,
                     )
                 }) else {
@@ -856,15 +865,17 @@ impl SettingsObserver {
 pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
     match kind {
         proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
-        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks(TaskKind::Script),
+        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
         proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
+        proto::LocalSettingsKind::Debug => LocalSettingsKind::Debug,
     }
 }
 
 pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
     match kind {
         LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
-        LocalSettingsKind::Tasks(_) => proto::LocalSettingsKind::Tasks,
+        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
         LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
+        LocalSettingsKind::Debug => proto::LocalSettingsKind::Debug,
     }
 }

crates/project/src/project_tests.rs 🔗

@@ -292,7 +292,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
         })
         .into_iter()
         .map(|(source_kind, task)| {
-            let resolved = task.resolved.unwrap();
+            let resolved = task.resolved;
             (
                 source_kind,
                 task.resolved_label,
@@ -359,7 +359,6 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
                         }])
                         .to_string(),
                     ),
-                    settings::TaskKind::Script,
                 )
                 .unwrap();
         });
@@ -370,7 +369,7 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
         .update(|cx| get_all_tasks(&project, &task_contexts, cx))
         .into_iter()
         .map(|(source_kind, task)| {
-            let resolved = task.resolved.unwrap();
+            let resolved = task.resolved;
             (
                 source_kind,
                 task.resolved_label,
@@ -495,7 +494,7 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
         active_worktree_tasks
             .into_iter()
             .map(|(source_kind, task)| {
-                let resolved = task.resolved.unwrap();
+                let resolved = task.resolved;
                 (source_kind, resolved.command)
             })
             .collect::<Vec<_>>(),

crates/project/src/task_inventory.rs 🔗

@@ -13,14 +13,15 @@ use collections::{HashMap, HashSet, VecDeque};
 use gpui::{App, AppContext as _, Entity, SharedString, Task};
 use itertools::Itertools;
 use language::{
-    ContextProvider, File, Language, LanguageToolchainStore, Location,
+    Buffer, ContextProvider, File, Language, LanguageToolchainStore, Location,
     language_settings::language_settings,
 };
 use lsp::{LanguageServerId, LanguageServerName};
-use settings::{InvalidSettingsError, TaskKind, parse_json_with_comments};
+use paths::{debug_task_file_name, task_file_name};
+use settings::{InvalidSettingsError, parse_json_with_comments};
 use task::{
-    DebugTaskTemplate, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates,
-    TaskVariables, VariableName,
+    DebugScenario, ResolvedTask, TaskContext, TaskId, TaskTemplate, TaskTemplates, TaskVariables,
+    VariableName,
 };
 use text::{BufferId, Point, ToPoint};
 use util::{NumericPrefixWithSuffix, ResultExt as _, paths::PathExt as _, post_inc};
@@ -32,13 +33,84 @@ use crate::{task_store::TaskSettingsLocation, worktree_store::WorktreeStore};
 #[derive(Debug, Default)]
 pub struct Inventory {
     last_scheduled_tasks: VecDeque<(TaskSourceKind, ResolvedTask)>,
-    templates_from_settings: ParsedTemplates,
+    templates_from_settings: InventoryFor<TaskTemplate>,
+    scenarios_from_settings: InventoryFor<DebugScenario>,
 }
 
-#[derive(Debug, Default)]
-struct ParsedTemplates {
-    global: HashMap<PathBuf, Vec<TaskTemplate>>,
-    worktree: HashMap<WorktreeId, HashMap<(Arc<Path>, TaskKind), Vec<TaskTemplate>>>,
+// Helper trait for better error messages in [InventoryFor]
+trait InventoryContents: Clone {
+    const GLOBAL_SOURCE_FILE: &'static str;
+    const LABEL: &'static str;
+}
+
+impl InventoryContents for TaskTemplate {
+    const GLOBAL_SOURCE_FILE: &'static str = "tasks.json";
+    const LABEL: &'static str = "tasks";
+}
+
+impl InventoryContents for DebugScenario {
+    const GLOBAL_SOURCE_FILE: &'static str = "debug.json";
+
+    const LABEL: &'static str = "debug scenarios";
+}
+
+#[derive(Debug)]
+struct InventoryFor<T> {
+    global: HashMap<PathBuf, Vec<T>>,
+    worktree: HashMap<WorktreeId, HashMap<Arc<Path>, Vec<T>>>,
+}
+
+impl<T: InventoryContents> InventoryFor<T> {
+    fn worktree_scenarios(
+        &self,
+        worktree: Option<WorktreeId>,
+    ) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
+        worktree.into_iter().flat_map(|worktree| {
+            self.worktree
+                .get(&worktree)
+                .into_iter()
+                .flatten()
+                .flat_map(|(directory, templates)| {
+                    templates.iter().map(move |template| (directory, template))
+                })
+                .map(move |(directory, template)| {
+                    (
+                        TaskSourceKind::Worktree {
+                            id: worktree,
+                            directory_in_worktree: directory.to_path_buf(),
+                            id_base: Cow::Owned(format!(
+                                "local worktree {} from directory {directory:?}",
+                                T::LABEL
+                            )),
+                        },
+                        template.clone(),
+                    )
+                })
+        })
+    }
+
+    fn global_scenarios(&self) -> impl '_ + Iterator<Item = (TaskSourceKind, T)> {
+        self.global.iter().flat_map(|(file_path, templates)| {
+            templates.into_iter().map(|template| {
+                (
+                    TaskSourceKind::AbsPath {
+                        id_base: Cow::Owned(format!("global {}", T::GLOBAL_SOURCE_FILE)),
+                        abs_path: file_path.clone(),
+                    },
+                    template.clone(),
+                )
+            })
+        })
+    }
+}
+
+impl<T> Default for InventoryFor<T> {
+    fn default() -> Self {
+        Self {
+            global: HashMap::default(),
+            worktree: HashMap::default(),
+        }
+    }
 }
 
 /// Kind of a source the tasks are fetched from, used to display more source information in the UI.
@@ -134,22 +206,40 @@ impl Inventory {
         cx.new(|_| Self::default())
     }
 
-    pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> {
-        self.templates_from_settings
-            .worktree
-            .values()
-            .flat_map(|tasks| {
-                tasks.iter().filter_map(|(kind, tasks)| {
-                    if matches!(kind.1, TaskKind::Debug) {
-                        Some(tasks)
-                    } else {
-                        None
-                    }
-                })
-            })
-            .flatten()
+    pub fn list_debug_scenarios(&self, worktree: Option<WorktreeId>) -> Vec<DebugScenario> {
+        let global_scenarios = self.global_debug_scenarios_from_settings();
+        let worktree_scenarios = self.worktree_scenarios_from_settings(worktree);
+
+        worktree_scenarios
+            .chain(global_scenarios)
+            .map(|(_, scenario)| scenario)
             .collect()
     }
+
+    pub fn task_template_by_label(
+        &self,
+        buffer: Option<Entity<Buffer>>,
+        label: &str,
+        cx: &App,
+    ) -> Option<TaskTemplate> {
+        let (worktree_id, file, language) = buffer
+            .map(|buffer| {
+                let buffer = buffer.read(cx);
+                let file = buffer.file().cloned();
+                (
+                    file.as_ref().map(|file| file.worktree_id(cx)),
+                    file,
+                    buffer.language().cloned(),
+                )
+            })
+            .unwrap_or((None, None, None));
+
+        self.list_tasks(file, language, worktree_id, cx)
+            .iter()
+            .find(|(_, template)| template.label == label)
+            .map(|val| val.1.clone())
+    }
+
     /// Pulls its task sources relevant to the worktree and the language given,
     /// returns all task templates with their source kinds, worktree tasks first, language tasks second
     /// and global tasks last. No specific order inside source kinds groups.
@@ -160,10 +250,11 @@ impl Inventory {
         worktree: Option<WorktreeId>,
         cx: &App,
     ) -> Vec<(TaskSourceKind, TaskTemplate)> {
+        let global_tasks = self.global_templates_from_settings();
+        let worktree_tasks = self.worktree_templates_from_settings(worktree);
         let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
             name: language.name().into(),
         });
-        let global_tasks = self.global_templates_from_settings();
         let language_tasks = language
             .filter(|language| {
                 language_settings(Some(language.name()), file.as_ref(), cx)
@@ -173,11 +264,11 @@ impl Inventory {
             .and_then(|language| language.context_provider()?.associated_tasks(file, cx))
             .into_iter()
             .flat_map(|tasks| tasks.0.into_iter())
-            .flat_map(|task| Some((task_source_kind.clone()?, task)))
-            .chain(global_tasks);
+            .flat_map(|task| Some((task_source_kind.clone()?, task)));
 
-        self.worktree_templates_from_settings(worktree)
+        worktree_tasks
             .chain(language_tasks)
+            .chain(global_tasks)
             .collect()
     }
 
@@ -358,51 +449,27 @@ impl Inventory {
     fn global_templates_from_settings(
         &self,
     ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
-        self.templates_from_settings
-            .global
-            .iter()
-            .flat_map(|(file_path, templates)| {
-                templates.into_iter().map(|template| {
-                    (
-                        TaskSourceKind::AbsPath {
-                            id_base: match template.task_type {
-                                task::TaskType::Script => Cow::Borrowed("global tasks.json"),
-                                task::TaskType::Debug(_) => Cow::Borrowed("global debug.json"),
-                            },
-                            abs_path: file_path.clone(),
-                        },
-                        template.clone(),
-                    )
-                })
-            })
+        self.templates_from_settings.global_scenarios()
+    }
+
+    fn global_debug_scenarios_from_settings(
+        &self,
+    ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
+        self.scenarios_from_settings.global_scenarios()
+    }
+
+    fn worktree_scenarios_from_settings(
+        &self,
+        worktree: Option<WorktreeId>,
+    ) -> impl '_ + Iterator<Item = (TaskSourceKind, DebugScenario)> {
+        self.scenarios_from_settings.worktree_scenarios(worktree)
     }
 
     fn worktree_templates_from_settings(
         &self,
         worktree: Option<WorktreeId>,
     ) -> impl '_ + Iterator<Item = (TaskSourceKind, TaskTemplate)> {
-        worktree.into_iter().flat_map(|worktree| {
-            self.templates_from_settings
-                .worktree
-                .get(&worktree)
-                .into_iter()
-                .flatten()
-                .flat_map(|(directory, templates)| {
-                    templates.iter().map(move |template| (directory, template))
-                })
-                .map(move |((directory, _task_kind), template)| {
-                    (
-                        TaskSourceKind::Worktree {
-                            id: worktree,
-                            directory_in_worktree: directory.to_path_buf(),
-                            id_base: Cow::Owned(format!(
-                                "local worktree tasks from directory {directory:?}"
-                            )),
-                        },
-                        template.clone(),
-                    )
-                })
-        })
+        self.templates_from_settings.worktree_scenarios(worktree)
     }
 
     /// Updates in-memory task metadata from the JSON string given.
@@ -413,7 +480,6 @@ impl Inventory {
         &mut self,
         location: TaskSettingsLocation<'_>,
         raw_tasks_json: Option<&str>,
-        task_kind: TaskKind,
     ) -> Result<(), InvalidSettingsError> {
         let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
             raw_tasks_json.unwrap_or("[]"),
@@ -424,21 +490,16 @@ impl Inventory {
                     path: match location {
                         TaskSettingsLocation::Global(path) => path.to_owned(),
                         TaskSettingsLocation::Worktree(settings_location) => {
-                            task_kind.config_in_dir(settings_location.path)
+                            settings_location.path.join(task_file_name())
                         }
                     },
                     message: format!("Failed to parse tasks file content as a JSON array: {e}"),
                 });
             }
         };
-        let new_templates = raw_tasks
-            .into_iter()
-            .filter_map(|raw_template| match &task_kind {
-                TaskKind::Script => serde_json::from_value::<TaskTemplate>(raw_template).log_err(),
-                TaskKind::Debug => serde_json::from_value::<DebugTaskTemplate>(raw_template)
-                    .log_err()
-                    .map(|content| content.to_zed_format()),
-            });
+        let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
+            serde_json::from_value::<TaskTemplate>(raw_template).log_err()
+        });
 
         let parsed_templates = &mut self.templates_from_settings;
         match location {
@@ -454,14 +515,72 @@ impl Inventory {
                     if let Some(worktree_tasks) =
                         parsed_templates.worktree.get_mut(&location.worktree_id)
                     {
-                        worktree_tasks.remove(&(Arc::from(location.path), task_kind));
+                        worktree_tasks.remove(location.path);
                     }
                 } else {
                     parsed_templates
                         .worktree
                         .entry(location.worktree_id)
                         .or_default()
-                        .insert((Arc::from(location.path), task_kind), new_templates);
+                        .insert(Arc::from(location.path), new_templates);
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    /// Updates in-memory task metadata from the JSON string given.
+    /// Will fail if the JSON is not a valid array of objects, but will continue if any object will not parse into a [`TaskTemplate`].
+    ///
+    /// Global tasks are updated for no worktree provided, otherwise the worktree metadata for a given path will be updated.
+    pub(crate) fn update_file_based_scenarios(
+        &mut self,
+        location: TaskSettingsLocation<'_>,
+        raw_tasks_json: Option<&str>,
+    ) -> Result<(), InvalidSettingsError> {
+        let raw_tasks = match parse_json_with_comments::<Vec<serde_json::Value>>(
+            raw_tasks_json.unwrap_or("[]"),
+        ) {
+            Ok(tasks) => tasks,
+            Err(e) => {
+                return Err(InvalidSettingsError::Debug {
+                    path: match location {
+                        TaskSettingsLocation::Global(path) => path.to_owned(),
+                        TaskSettingsLocation::Worktree(settings_location) => {
+                            settings_location.path.join(debug_task_file_name())
+                        }
+                    },
+                    message: format!("Failed to parse tasks file content as a JSON array: {e}"),
+                });
+            }
+        };
+        let new_templates = raw_tasks.into_iter().filter_map(|raw_template| {
+            serde_json::from_value::<DebugScenario>(raw_template).log_err()
+        });
+
+        let parsed_scenarios = &mut self.scenarios_from_settings;
+        match location {
+            TaskSettingsLocation::Global(path) => {
+                parsed_scenarios
+                    .global
+                    .entry(path.to_owned())
+                    .insert_entry(new_templates.collect());
+            }
+            TaskSettingsLocation::Worktree(location) => {
+                let new_templates = new_templates.collect::<Vec<_>>();
+                if new_templates.is_empty() {
+                    if let Some(worktree_tasks) =
+                        parsed_scenarios.worktree.get_mut(&location.worktree_id)
+                    {
+                        worktree_tasks.remove(location.path);
+                    }
+                } else {
+                    parsed_scenarios
+                        .worktree
+                        .entry(location.worktree_id)
+                        .or_default()
+                        .insert(Arc::from(location.path), new_templates);
                 }
             }
         }
@@ -677,6 +796,10 @@ impl ContextProvider for BasicContextProvider {
 
         Task::ready(Ok(task_variables))
     }
+
+    fn debug_adapter(&self) -> Option<String> {
+        None
+    }
 }
 
 /// A ContextProvider that doesn't provide any task variables on it's own, though it has some associated tasks.
@@ -700,6 +823,10 @@ impl ContextProvider for ContextProviderWithTasks {
     ) -> Option<TaskTemplates> {
         Some(self.templates.clone())
     }
+
+    fn debug_adapter(&self) -> Option<String> {
+        None
+    }
 }
 
 #[cfg(test)]
@@ -744,7 +871,6 @@ mod tests {
                     Some(&mock_tasks_from_names(
                         expected_initial_state.iter().map(|name| name.as_str()),
                     )),
-                    settings::TaskKind::Script,
                 )
                 .unwrap();
         });
@@ -800,7 +926,6 @@ mod tests {
                             .into_iter()
                             .chain(expected_initial_state.iter().map(|name| name.as_str())),
                     )),
-                    settings::TaskKind::Script,
                 )
                 .unwrap();
         });
@@ -925,7 +1050,6 @@ mod tests {
                             .iter()
                             .map(|(_, name)| name.as_str()),
                     )),
-                    settings::TaskKind::Script,
                 )
                 .unwrap();
             inventory
@@ -937,7 +1061,6 @@ mod tests {
                     Some(&mock_tasks_from_names(
                         worktree_1_tasks.iter().map(|(_, name)| name.as_str()),
                     )),
-                    settings::TaskKind::Script,
                 )
                 .unwrap();
             inventory
@@ -949,7 +1072,6 @@ mod tests {
                     Some(&mock_tasks_from_names(
                         worktree_2_tasks.iter().map(|(_, name)| name.as_str()),
                     )),
-                    settings::TaskKind::Script,
                 )
                 .unwrap();
         });

crates/project/src/task_store.rs 🔗

@@ -11,7 +11,7 @@ use language::{
     proto::{deserialize_anchor, serialize_anchor},
 };
 use rpc::{AnyProtoClient, TypedEnvelope, proto};
-use settings::{InvalidSettingsError, SettingsLocation, TaskKind};
+use settings::{InvalidSettingsError, SettingsLocation};
 use task::{TaskContext, TaskVariables, VariableName};
 use text::{BufferId, OffsetRangeExt};
 use util::ResultExt;
@@ -264,7 +264,6 @@ impl TaskStore {
         &self,
         location: TaskSettingsLocation<'_>,
         raw_tasks_json: Option<&str>,
-        task_type: TaskKind,
         cx: &mut Context<Self>,
     ) -> Result<(), InvalidSettingsError> {
         let task_inventory = match self {
@@ -276,7 +275,26 @@ impl TaskStore {
             .filter(|json| !json.is_empty());
 
         task_inventory.update(cx, |inventory, _| {
-            inventory.update_file_based_tasks(location, raw_tasks_json, task_type)
+            inventory.update_file_based_tasks(location, raw_tasks_json)
+        })
+    }
+
+    pub(super) fn update_user_debug_scenarios(
+        &self,
+        location: TaskSettingsLocation<'_>,
+        raw_tasks_json: Option<&str>,
+        cx: &mut Context<Self>,
+    ) -> Result<(), InvalidSettingsError> {
+        let task_inventory = match self {
+            TaskStore::Functional(state) => &state.task_inventory,
+            TaskStore::Noop => return Ok(()),
+        };
+        let raw_tasks_json = raw_tasks_json
+            .map(|json| json.trim())
+            .filter(|json| !json.is_empty());
+
+        task_inventory.update(cx, |inventory, _| {
+            inventory.update_file_based_scenarios(location, raw_tasks_json)
         })
     }
 }

crates/proto/proto/debugger.proto 🔗

@@ -543,6 +543,7 @@ message DebugLaunchRequest {
     string program = 1;
     optional string cwd = 2;
     repeated string args = 3;
+    map<string, string> env = 4;
 }
 
 message DebugAttachRequest {
@@ -558,7 +559,7 @@ message DapModuleId {
 
 message GetDebugAdapterBinary {
     uint64 project_id = 1;
-    DebugTaskDefinition task = 2;
+    DebugTaskDefinition definition = 2;
 }
 
 message DebugAdapterBinary {
@@ -575,8 +576,32 @@ message DebugAdapterBinary {
     }
 }
 
-message RunDebugLocator {
+message RunDebugLocators {
     uint64 project_id = 1;
-    string locator = 2;
-    DebugTaskDefinition task = 3;
+    SpawnInTerminal build_command = 2;
+}
+
+message DebugRequest {
+    oneof request {
+        DebugLaunchRequest debug_launch_request = 1;
+        DebugAttachRequest debug_attach_request = 2;
+    }
+}
+
+message DebugScenario {
+    string label = 1;
+    string adapter = 2;
+    reserved 3;
+    DebugRequest request = 4;
+    optional TcpHost connection = 5;
+    optional bool stop_on_entry = 6;
+    optional string configuration = 7;
+}
+
+message SpawnInTerminal {
+    string label = 1;
+    string command = 2;
+    repeated string args = 3;
+    map<string, string> env = 4;
+    optional string cwd = 5;
 }

crates/proto/proto/zed.proto 🔗

@@ -377,8 +377,8 @@ message Envelope {
 
         GetDebugAdapterBinary get_debug_adapter_binary = 339;
         DebugAdapterBinary debug_adapter_binary = 340;
-        RunDebugLocator run_debug_locator = 341;
-        DebugTaskDefinition debug_task_definition = 342; // current max
+        RunDebugLocators run_debug_locators = 341;
+        DebugRequest debug_request = 342; // current max
     }
 
     reserved 87 to 88;

crates/proto/src/proto.rs 🔗

@@ -298,8 +298,8 @@ messages!(
     (GitInit, Background),
     (GetDebugAdapterBinary, Background),
     (DebugAdapterBinary, Background),
-    (RunDebugLocator, Background),
-    (DebugTaskDefinition, Background),
+    (RunDebugLocators, Background),
+    (DebugRequest, Background),
 );
 
 request_messages!(
@@ -456,7 +456,7 @@ request_messages!(
     (GitInit, Ack),
     (ToggleBreakpoint, Ack),
     (GetDebugAdapterBinary, DebugAdapterBinary),
-    (RunDebugLocator, DebugTaskDefinition),
+    (RunDebugLocators, DebugRequest),
 );
 
 entity_messages!(
@@ -576,7 +576,7 @@ entity_messages!(
     GitInit,
     BreakpointsForFile,
     ToggleBreakpoint,
-    RunDebugLocator,
+    RunDebugLocators,
     GetDebugAdapterBinary,
 );
 

crates/remote_server/src/headless_project.rs 🔗

@@ -245,7 +245,7 @@ impl HeadlessProject {
         LspStore::init(&client);
         TaskStore::init(Some(&client));
         ToolchainStore::init(&client);
-        DapStore::init(&client);
+        DapStore::init(&client, cx);
         // todo(debugger): Re init breakpoint store when we set it up for collab
         // BreakpointStore::init(&client);
         GitStore::init(&client);

crates/settings/src/settings.rs 🔗

@@ -20,7 +20,7 @@ pub use keymap_file::{
 pub use settings_file::*;
 pub use settings_store::{
     InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
-    SettingsStore, TaskKind, parse_json_with_comments,
+    SettingsStore, parse_json_with_comments,
 };
 pub use vscode_import::VsCodeSettings;
 

crates/settings/src/settings_store.rs 🔗

@@ -5,9 +5,7 @@ use fs::Fs;
 use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture};
 use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal};
 
-use paths::{
-    EDITORCONFIG_NAME, debug_task_file_name, local_settings_file_relative_path, task_file_name,
-};
+use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
 use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema};
 use serde::{Deserialize, Serialize, de::DeserializeOwned};
 use serde_json::Value;
@@ -217,14 +215,9 @@ impl FromStr for Editorconfig {
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub enum LocalSettingsKind {
     Settings,
-    Tasks(TaskKind),
+    Tasks,
     Editorconfig,
-}
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
-pub enum TaskKind {
     Debug,
-    Script,
 }
 
 impl Global for SettingsStore {}
@@ -265,16 +258,6 @@ trait AnySettingValue: 'static + Send + Sync {
 
 struct DeserializedSetting(Box<dyn Any>);
 
-impl TaskKind {
-    /// Returns a file path of a task configuration file of this kind within the given directory.
-    pub fn config_in_dir(&self, dir: &Path) -> PathBuf {
-        dir.join(match self {
-            Self::Debug => debug_task_file_name(),
-            Self::Script => task_file_name(),
-        })
-    }
-}
-
 impl SettingsStore {
     pub fn new(cx: &App) -> Self {
         let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
@@ -684,10 +667,17 @@ impl SettingsStore {
                 .map(|content| content.trim())
                 .filter(|content| !content.is_empty()),
         ) {
-            (LocalSettingsKind::Tasks(task_kind), _) => {
+            (LocalSettingsKind::Tasks, _) => {
                 return Err(InvalidSettingsError::Tasks {
                     message: "Attempted to submit tasks into the settings store".to_string(),
-                    path: task_kind.config_in_dir(&directory_path),
+                    path: directory_path.join(task_file_name()),
+                });
+            }
+            (LocalSettingsKind::Debug, _) => {
+                return Err(InvalidSettingsError::Debug {
+                    message: "Attempted to submit debugger config into the settings store"
+                        .to_string(),
+                    path: directory_path.join(task_file_name()),
                 });
             }
             (LocalSettingsKind::Settings, None) => {
@@ -1085,6 +1075,7 @@ pub enum InvalidSettingsError {
     DefaultSettings { message: String },
     Editorconfig { path: PathBuf, message: String },
     Tasks { path: PathBuf, message: String },
+    Debug { path: PathBuf, message: String },
 }
 
 impl std::fmt::Display for InvalidSettingsError {
@@ -1095,7 +1086,8 @@ impl std::fmt::Display for InvalidSettingsError {
             | InvalidSettingsError::ServerSettings { message }
             | InvalidSettingsError::DefaultSettings { message }
             | InvalidSettingsError::Tasks { message, .. }
-            | InvalidSettingsError::Editorconfig { message, .. } => {
+            | InvalidSettingsError::Editorconfig { message, .. }
+            | InvalidSettingsError::Debug { message, .. } => {
                 write!(f, "{message}")
             }
         }

crates/task/src/debug_format.rs 🔗

@@ -1,11 +1,11 @@
 use anyhow::Result;
+use collections::FxHashMap;
+use gpui::SharedString;
 use schemars::{JsonSchema, r#gen::SchemaSettings};
 use serde::{Deserialize, Serialize};
 use std::path::PathBuf;
 use std::{net::Ipv4Addr, path::Path};
 
-use crate::{TaskTemplate, TaskType, task_template::DebugArgs};
-
 /// Represents the host information of the debug adapter
 #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 pub struct TcpArgumentsTemplate {
@@ -63,6 +63,8 @@ pub struct LaunchRequest {
     /// Arguments to pass to a debuggee
     #[serde(default)]
     pub args: Vec<String>,
+    #[serde(default)]
+    pub env: FxHashMap<String, String>,
 }
 
 /// Represents the type that will determine which request to call on the debug adapter
@@ -75,6 +77,64 @@ pub enum DebugRequest {
     Attach(AttachRequest),
 }
 
+impl DebugRequest {
+    pub fn to_proto(&self) -> proto::DebugRequest {
+        match self {
+            DebugRequest::Launch(launch_request) => proto::DebugRequest {
+                request: Some(proto::debug_request::Request::DebugLaunchRequest(
+                    proto::DebugLaunchRequest {
+                        program: launch_request.program.clone(),
+                        cwd: launch_request
+                            .cwd
+                            .as_ref()
+                            .map(|cwd| cwd.to_string_lossy().into_owned()),
+                        args: launch_request.args.clone(),
+                        env: launch_request
+                            .env
+                            .iter()
+                            .map(|(k, v)| (k.clone(), v.clone()))
+                            .collect(),
+                    },
+                )),
+            },
+            DebugRequest::Attach(attach_request) => proto::DebugRequest {
+                request: Some(proto::debug_request::Request::DebugAttachRequest(
+                    proto::DebugAttachRequest {
+                        process_id: attach_request
+                            .process_id
+                            .expect("The process ID to be already filled out."),
+                    },
+                )),
+            },
+        }
+    }
+
+    pub fn from_proto(val: proto::DebugRequest) -> Result<DebugRequest> {
+        let request = val
+            .request
+            .ok_or_else(|| anyhow::anyhow!("Missing debug request"))?;
+        match request {
+            proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest {
+                program,
+                cwd,
+                args,
+                env,
+            }) => Ok(DebugRequest::Launch(LaunchRequest {
+                program,
+                cwd: cwd.map(From::from),
+                args,
+                env: env.into_iter().collect(),
+            })),
+
+            proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest {
+                process_id,
+            }) => Ok(DebugRequest::Attach(AttachRequest {
+                process_id: Some(process_id),
+            })),
+        }
+    }
+}
+
 impl From<LaunchRequest> for DebugRequest {
     fn from(launch_config: LaunchRequest) -> Self {
         DebugRequest::Launch(launch_config)
@@ -87,180 +147,46 @@ impl From<AttachRequest> for DebugRequest {
     }
 }
 
-impl TryFrom<TaskTemplate> for DebugTaskTemplate {
-    type Error = ();
-
-    fn try_from(value: TaskTemplate) -> Result<Self, Self::Error> {
-        let TaskType::Debug(debug_args) = value.task_type else {
-            return Err(());
-        };
-
-        let request = match debug_args.request {
-            crate::DebugArgsRequest::Launch => DebugRequest::Launch(LaunchRequest {
-                program: value.command,
-                cwd: value.cwd.map(PathBuf::from),
-                args: value.args,
-            }),
-            crate::DebugArgsRequest::Attach(attach_config) => DebugRequest::Attach(attach_config),
-        };
-
-        Ok(DebugTaskTemplate {
-            locator: debug_args.locator,
-            definition: DebugTaskDefinition {
-                adapter: debug_args.adapter,
-                request,
-                label: value.label,
-                initialize_args: debug_args.initialize_args,
-                tcp_connection: debug_args.tcp_connection,
-                stop_on_entry: debug_args.stop_on_entry,
-            },
-        })
-    }
-}
-
-impl DebugTaskTemplate {
-    /// Translate from debug definition to a task template
-    pub fn to_zed_format(self) -> TaskTemplate {
-        let (command, cwd, request) = match self.definition.request {
-            DebugRequest::Launch(launch_config) => (
-                launch_config.program,
-                launch_config
-                    .cwd
-                    .map(|cwd| cwd.to_string_lossy().to_string()),
-                crate::task_template::DebugArgsRequest::Launch,
-            ),
-            DebugRequest::Attach(attach_config) => (
-                "".to_owned(),
-                None,
-                crate::task_template::DebugArgsRequest::Attach(attach_config),
-            ),
-        };
-
-        let task_type = TaskType::Debug(DebugArgs {
-            adapter: self.definition.adapter,
-            request,
-            initialize_args: self.definition.initialize_args,
-            locator: self.locator,
-            tcp_connection: self.definition.tcp_connection,
-            stop_on_entry: self.definition.stop_on_entry,
-        });
-
-        let label = self.definition.label.clone();
-
-        TaskTemplate {
-            label,
-            command,
-            args: vec![],
-            task_type,
-            cwd,
-            ..Default::default()
-        }
-    }
-}
-
-#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
-#[serde(rename_all = "snake_case")]
-pub struct DebugTaskTemplate {
-    pub locator: Option<String>,
-    #[serde(flatten)]
-    pub definition: DebugTaskDefinition,
-}
-
 /// This struct represent a user created debug task
 #[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 #[serde(rename_all = "snake_case")]
-pub struct DebugTaskDefinition {
-    /// The adapter to run
-    pub adapter: String,
-    /// The type of request that should be called on the debug adapter
-    #[serde(flatten)]
-    pub request: DebugRequest,
+pub struct DebugScenario {
+    pub adapter: SharedString,
     /// Name of the debug task
-    pub label: String,
+    pub label: SharedString,
+    /// A task to run prior to spawning the debuggee.
+    pub build: Option<SharedString>,
+    #[serde(flatten)]
+    pub request: Option<DebugRequest>,
     /// Additional initialization arguments to be sent on DAP initialization
+    #[serde(default)]
     pub initialize_args: Option<serde_json::Value>,
     /// Optional TCP connection information
     ///
     /// If provided, this will be used to connect to the debug adapter instead of
     /// spawning a new process. This is useful for connecting to a debug adapter
     /// that is already running or is started by another process.
+    #[serde(default)]
     pub tcp_connection: Option<TcpArgumentsTemplate>,
     /// Whether to tell the debug adapter to stop on entry
+    #[serde(default)]
     pub stop_on_entry: Option<bool>,
 }
 
-impl DebugTaskDefinition {
+impl DebugScenario {
     pub fn cwd(&self) -> Option<&Path> {
-        if let DebugRequest::Launch(config) = &self.request {
-            config.cwd.as_deref()
+        if let Some(DebugRequest::Launch(config)) = &self.request {
+            config.cwd.as_ref().map(Path::new)
         } else {
             None
         }
     }
-    pub fn to_proto(&self) -> proto::DebugTaskDefinition {
-        proto::DebugTaskDefinition {
-            adapter: self.adapter.clone(),
-            request: Some(match &self.request {
-                DebugRequest::Launch(config) => {
-                    proto::debug_task_definition::Request::DebugLaunchRequest(
-                        proto::DebugLaunchRequest {
-                            program: config.program.clone(),
-                            cwd: config.cwd.as_ref().map(|c| c.to_string_lossy().to_string()),
-                            args: config.args.clone(),
-                        },
-                    )
-                }
-                DebugRequest::Attach(attach_request) => {
-                    proto::debug_task_definition::Request::DebugAttachRequest(
-                        proto::DebugAttachRequest {
-                            process_id: attach_request.process_id.unwrap_or_default(),
-                        },
-                    )
-                }
-            }),
-            label: self.label.clone(),
-            initialize_args: self.initialize_args.as_ref().map(|v| v.to_string()),
-            tcp_connection: self.tcp_connection.as_ref().map(|t| t.to_proto()),
-            stop_on_entry: self.stop_on_entry,
-        }
-    }
-
-    pub fn from_proto(proto: proto::DebugTaskDefinition) -> Result<Self> {
-        let request = proto
-            .request
-            .ok_or_else(|| anyhow::anyhow!("request is required"))?;
-        Ok(Self {
-            label: proto.label,
-            initialize_args: proto.initialize_args.map(|v| v.into()),
-            tcp_connection: proto
-                .tcp_connection
-                .map(TcpArgumentsTemplate::from_proto)
-                .transpose()?,
-            stop_on_entry: proto.stop_on_entry,
-            adapter: proto.adapter.clone(),
-            request: match request {
-                proto::debug_task_definition::Request::DebugAttachRequest(config) => {
-                    DebugRequest::Attach(AttachRequest {
-                        process_id: Some(config.process_id),
-                    })
-                }
-
-                proto::debug_task_definition::Request::DebugLaunchRequest(config) => {
-                    DebugRequest::Launch(LaunchRequest {
-                        program: config.program,
-                        cwd: config.cwd.map(|cwd| cwd.into()),
-                        args: config.args,
-                    })
-                }
-            },
-        })
-    }
 }
 
 /// A group of Debug Tasks defined in a JSON file.
 #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
 #[serde(transparent)]
-pub struct DebugTaskFile(pub Vec<DebugTaskTemplate>);
+pub struct DebugTaskFile(pub Vec<DebugScenario>);
 
 impl DebugTaskFile {
     /// Generates JSON schema of Tasks JSON template format.

crates/task/src/lib.rs 🔗

@@ -16,12 +16,10 @@ use std::path::PathBuf;
 use std::str::FromStr;
 
 pub use debug_format::{
-    AttachRequest, DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate,
-    LaunchRequest, TcpArgumentsTemplate,
+    AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate,
 };
 pub use task_template::{
-    DebugArgs, DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate,
-    TaskTemplates, TaskType,
+    DebugArgsRequest, HideStrategy, RevealStrategy, TaskModal, TaskTemplate, TaskTemplates,
 };
 pub use vscode_debug_format::VsCodeDebugTaskFile;
 pub use vscode_format::VsCodeTaskFile;
@@ -29,11 +27,11 @@ pub use zed_actions::RevealTarget;
 
 /// Task identifier, unique within the application.
 /// Based on it, task reruns and terminal tabs are managed.
-#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
+#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)]
 pub struct TaskId(pub String);
 
 /// Contains all information needed by Zed to spawn a new terminal tab for the given task.
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
 pub struct SpawnInTerminal {
     /// Id of the task to use when determining task tab affinity.
     pub id: TaskId,
@@ -72,6 +70,36 @@ pub struct SpawnInTerminal {
     pub show_rerun: bool,
 }
 
+impl SpawnInTerminal {
+    pub fn to_proto(&self) -> proto::SpawnInTerminal {
+        proto::SpawnInTerminal {
+            label: self.label.clone(),
+            command: self.command.clone(),
+            args: self.args.clone(),
+            env: self
+                .env
+                .iter()
+                .map(|(k, v)| (k.clone(), v.clone()))
+                .collect(),
+            cwd: self
+                .cwd
+                .clone()
+                .map(|cwd| cwd.to_string_lossy().into_owned()),
+        }
+    }
+
+    pub fn from_proto(proto: proto::SpawnInTerminal) -> Self {
+        Self {
+            label: proto.label.clone(),
+            command: proto.command.clone(),
+            args: proto.args.clone(),
+            env: proto.env.into_iter().collect(),
+            cwd: proto.cwd.map(PathBuf::from).clone(),
+            ..Default::default()
+        }
+    }
+}
+
 /// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task.
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct ResolvedTask {
@@ -89,7 +117,7 @@ pub struct ResolvedTask {
     substituted_variables: HashSet<VariableName>,
     /// Further actions that need to take place after the resolved task is spawned,
     /// with all task variables resolved.
-    pub resolved: Option<SpawnInTerminal>,
+    pub resolved: SpawnInTerminal,
 }
 
 impl ResolvedTask {
@@ -98,63 +126,6 @@ impl ResolvedTask {
         &self.original_task
     }
 
-    /// Get the task type that determines what this task is used for
-    /// And where is it shown in the UI
-    pub fn task_type(&self) -> TaskType {
-        self.original_task.task_type.clone()
-    }
-
-    /// Get the configuration for the debug adapter that should be used for this task.
-    pub fn resolved_debug_adapter_config(&self) -> Option<DebugTaskTemplate> {
-        match self.original_task.task_type.clone() {
-            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
-                        }
-                    })
-                    .collect();
-
-                Some(DebugTaskTemplate {
-                    locator: debug_args.locator.clone(),
-                    definition: DebugTaskDefinition {
-                        label: resolved.label.clone(),
-                        adapter: debug_args.adapter.clone(),
-                        request: match debug_args.request {
-                            crate::task_template::DebugArgsRequest::Launch => {
-                                DebugRequest::Launch(LaunchRequest {
-                                    program: resolved.command.clone(),
-                                    cwd: resolved.cwd.clone(),
-                                    args,
-                                })
-                            }
-                            crate::task_template::DebugArgsRequest::Attach(attach_config) => {
-                                DebugRequest::Attach(attach_config)
-                            }
-                        },
-                        initialize_args: debug_args.initialize_args,
-                        tcp_connection: debug_args.tcp_connection,
-                        stop_on_entry: debug_args.stop_on_entry,
-                    },
-                })
-            }
-            _ => None,
-        }
-    }
-
     /// Variables that were substituted during the task template resolution.
     pub fn substituted_variables(&self) -> &HashSet<VariableName> {
         &self.substituted_variables
@@ -162,10 +133,7 @@ impl ResolvedTask {
 
     /// A human-readable label to display in the UI.
     pub fn display_label(&self) -> &str {
-        self.resolved
-            .as_ref()
-            .map(|resolved| resolved.label.as_str())
-            .unwrap_or_else(|| self.resolved_label.as_str())
+        self.resolved.label.as_str()
     }
 }
 

crates/task/src/task_template.rs 🔗

@@ -7,7 +7,6 @@ use std::path::PathBuf;
 use util::serde::default_true;
 use util::{ResultExt, truncate_and_remove_front};
 
-use crate::debug_format::TcpArgumentsTemplate;
 use crate::{
     AttachRequest, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId,
     VariableName, ZED_VARIABLE_NAME_PREFIX,
@@ -59,9 +58,6 @@ pub struct TaskTemplate {
     /// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`.
     #[serde(default)]
     pub hide: HideStrategy,
-    /// If this task should start a debugger or not
-    #[serde(default, skip)]
-    pub task_type: TaskType,
     /// Represents the tags which this template attaches to.
     /// Adding this removes this task from other UI and gives you ability to run it by tag.
     #[serde(default, deserialize_with = "non_empty_string_vec")]
@@ -87,34 +83,6 @@ pub enum DebugArgsRequest {
     Attach(AttachRequest),
 }
 
-#[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<TcpArgumentsTemplate>,
-    /// Args to send to debug adapter
-    pub initialize_args: Option<serde_json::value::Value>,
-    /// the locator to use
-    pub locator: Option<String>,
-    /// Whether to tell the debug adapter to stop on entry
-    pub stop_on_entry: Option<bool>,
-}
-
-/// Represents the type of task that is being ran
-#[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(DebugArgs),
-}
-
 #[derive(Clone, Debug, PartialEq, Eq)]
 /// The type of task modal to spawn
 pub enum TaskModal {
@@ -174,9 +142,7 @@ impl TaskTemplate {
     /// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources),
     /// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details.
     pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option<ResolvedTask> {
-        if self.label.trim().is_empty()
-            || (self.command.trim().is_empty() && matches!(self.task_type, TaskType::Script))
-        {
+        if self.label.trim().is_empty() || self.command.trim().is_empty() {
             return None;
         }
 
@@ -285,7 +251,7 @@ impl TaskTemplate {
             substituted_variables,
             original_task: self.clone(),
             resolved_label: full_label.clone(),
-            resolved: Some(SpawnInTerminal {
+            resolved: SpawnInTerminal {
                 id,
                 cwd,
                 full_label,
@@ -310,7 +276,7 @@ impl TaskTemplate {
                 show_summary: self.show_summary,
                 show_command: self.show_command,
                 show_rerun: true,
-            }),
+            },
         })
     }
 }
@@ -474,12 +440,7 @@ mod tests {
                 .resolve_task(TEST_ID_BASE, task_cx)
                 .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}"));
             assert_substituted_variables(&resolved_task, Vec::new());
-            resolved_task
-                .resolved
-                .clone()
-                .unwrap_or_else(|| {
-                    panic!("failed to get resolve data for resolved task. Template: {task_without_cwd:?} Resolved: {resolved_task:?}")
-                })
+            resolved_task.resolved
         };
 
         let cx = TaskContext {
@@ -626,10 +587,7 @@ mod tests {
                 all_variables.iter().map(|(name, _)| name.clone()).collect(),
             );
 
-            let spawn_in_terminal = resolved_task
-                .resolved
-                .as_ref()
-                .expect("should have resolved a spawn in terminal task");
+            let spawn_in_terminal = &resolved_task.resolved;
             assert_eq!(
                 spawn_in_terminal.label,
                 format!(
@@ -713,7 +671,7 @@ mod tests {
             .resolve_task(TEST_ID_BASE, &TaskContext::default())
             .unwrap();
         assert_substituted_variables(&resolved_task, Vec::new());
-        let resolved = resolved_task.resolved.unwrap();
+        let resolved = resolved_task.resolved;
         assert_eq!(resolved.label, task.label);
         assert_eq!(resolved.command, task.command);
         assert_eq!(resolved.args, task.args);
@@ -882,8 +840,7 @@ mod tests {
         let resolved = template
             .resolve_task(TEST_ID_BASE, &context)
             .unwrap()
-            .resolved
-            .unwrap();
+            .resolved;
 
         assert_eq!(resolved.env["TASK_ENV_VAR1"], "TASK_ENV_VAR1_VALUE");
         assert_eq!(resolved.env["TASK_ENV_VAR2"], "env_var_2 1234 5678");

crates/task/src/vscode_debug_format.rs 🔗

@@ -2,12 +2,13 @@ use std::path::PathBuf;
 
 use anyhow::anyhow;
 use collections::HashMap;
+use gpui::SharedString;
 use serde::Deserialize;
 use util::ResultExt as _;
 
 use crate::{
-    AttachRequest, DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate,
-    EnvVariableReplacer, LaunchRequest, TcpArgumentsTemplate, VariableName,
+    AttachRequest, DebugRequest, DebugScenario, DebugTaskFile, EnvVariableReplacer, LaunchRequest,
+    TcpArgumentsTemplate, VariableName,
 };
 
 #[derive(Clone, Debug, Deserialize, PartialEq)]
@@ -43,11 +44,12 @@ struct VsCodeDebugTaskDefinition {
 }
 
 impl VsCodeDebugTaskDefinition {
-    fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugTaskTemplate> {
-        let label = replacer.replace(&self.name);
+    fn try_to_zed(self, replacer: &EnvVariableReplacer) -> anyhow::Result<DebugScenario> {
+        let label = replacer.replace(&self.name).into();
         // TODO based on grep.app results it seems that vscode supports whitespace-splitting this field (ugh)
-        let definition = DebugTaskDefinition {
+        let definition = DebugScenario {
             label,
+            build: None,
             request: match self.request {
                 Request::Launch => {
                     let cwd = self.cwd.map(|cwd| PathBuf::from(replacer.replace(&cwd)));
@@ -60,11 +62,22 @@ impl VsCodeDebugTaskDefinition {
                         .into_iter()
                         .map(|arg| replacer.replace(&arg))
                         .collect();
-                    DebugRequest::Launch(LaunchRequest { program, cwd, args })
+                    let env = self
+                        .env
+                        .into_iter()
+                        .filter_map(|(k, v)| v.map(|v| (k, v)))
+                        .collect();
+                    DebugRequest::Launch(LaunchRequest {
+                        program,
+                        cwd,
+                        args,
+                        env,
+                    })
+                    .into()
                 }
-                Request::Attach => DebugRequest::Attach(AttachRequest { process_id: None }),
+                Request::Attach => DebugRequest::Attach(AttachRequest { process_id: None }).into(),
             },
-            adapter: task_type_to_adapter_name(self.r#type),
+            adapter: task_type_to_adapter_name(&self.r#type),
             // TODO host?
             tcp_connection: self.port.map(|port| TcpArgumentsTemplate {
                 port: Some(port),
@@ -75,11 +88,7 @@ impl VsCodeDebugTaskDefinition {
             // TODO
             initialize_args: None,
         };
-        let template = DebugTaskTemplate {
-            locator: None,
-            definition,
-        };
-        Ok(template)
+        Ok(definition)
     }
 }
 
@@ -110,24 +119,26 @@ impl TryFrom<VsCodeDebugTaskFile> for DebugTaskFile {
     }
 }
 
-// TODO figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here
-fn task_type_to_adapter_name(task_type: String) -> String {
-    match task_type.as_str() {
-        "node" => "JavaScript".to_owned(),
-        "go" => "Delve".to_owned(),
-        "php" => "PHP".to_owned(),
-        "cppdbg" | "lldb" => "CodeLLDB".to_owned(),
-        "debugpy" => "Debugpy".to_owned(),
+// todo(debugger) figure out how to make JsDebugAdapter::ADAPTER_NAME et al available here
+fn task_type_to_adapter_name(task_type: &str) -> SharedString {
+    match task_type {
+        "node" => "JavaScript",
+        "go" => "Delve",
+        "php" => "PHP",
+        "cppdbg" | "lldb" => "CodeLLDB",
+        "debugpy" => "Debugpy",
         _ => task_type,
     }
+    .to_owned()
+    .into()
 }
 
 #[cfg(test)]
 mod tests {
-    use crate::{
-        DebugRequest, DebugTaskDefinition, DebugTaskFile, DebugTaskTemplate, LaunchRequest,
-        TcpArgumentsTemplate,
-    };
+
+    use collections::FxHashMap;
+
+    use crate::{DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, TcpArgumentsTemplate};
 
     use super::VsCodeDebugTaskFile;
 
@@ -159,24 +170,23 @@ mod tests {
         let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates");
         pretty_assertions::assert_eq!(
             zed,
-            DebugTaskFile(vec![DebugTaskTemplate {
-                locator: None,
-                definition: DebugTaskDefinition {
-                    label: "Debug my JS app".into(),
-                    adapter: "JavaScript".into(),
-                    stop_on_entry: Some(true),
-                    initialize_args: None,
-                    tcp_connection: Some(TcpArgumentsTemplate {
-                        port: Some(17),
-                        host: None,
-                        timeout: None,
-                    }),
-                    request: DebugRequest::Launch(LaunchRequest {
-                        program: "${ZED_WORKTREE_ROOT}/xyz.js".into(),
-                        args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()],
-                        cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()),
-                    }),
-                }
+            DebugTaskFile(vec![DebugScenario {
+                label: "Debug my JS app".into(),
+                adapter: "JavaScript".into(),
+                stop_on_entry: Some(true),
+                initialize_args: None,
+                tcp_connection: Some(TcpArgumentsTemplate {
+                    port: Some(17),
+                    host: None,
+                    timeout: None,
+                }),
+                request: Some(DebugRequest::Launch(LaunchRequest {
+                    program: "${ZED_WORKTREE_ROOT}/xyz.js".into(),
+                    args: vec!["--foo".into(), "${ZED_WORKTREE_ROOT}/thing".into()],
+                    cwd: Some("${ZED_WORKTREE_ROOT}/${FOO}/sub".into()),
+                    env: FxHashMap::from_iter([("X".into(), "Y".into())])
+                })),
+                build: None
             }])
         );
     }

crates/tasks_ui/src/modal.rs 🔗

@@ -10,10 +10,7 @@ use gpui::{
 use itertools::Itertools;
 use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
 use project::{TaskSourceKind, task_store::TaskStore};
-use task::{
-    DebugRequest, DebugTaskDefinition, ResolvedTask, RevealTarget, TaskContext, TaskModal,
-    TaskTemplate, TaskType,
-};
+use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskModal, TaskTemplate};
 use ui::{
     ActiveTheme, Button, ButtonCommon, ButtonSize, Clickable, Color, FluentBuilder as _, Icon,
     IconButton, IconButtonShape, IconName, IconSize, IntoElement, KeyBinding, Label, LabelSize,
@@ -187,7 +184,7 @@ impl Render for TasksModal {
 }
 
 pub struct ShowAttachModal {
-    pub debug_config: DebugTaskDefinition,
+    pub debug_config: DebugScenario,
 }
 
 impl EventEmitter<DismissEvent> for TasksModal {}
@@ -354,48 +351,20 @@ impl PickerDelegate for TasksModalDelegate {
             reveal_target: Some(reveal_target),
         }) = &self.task_overrides
         {
-            if let Some(resolved_task) = &mut task.resolved {
-                resolved_task.reveal_target = *reveal_target;
-            }
+            task.resolved.reveal_target = *reveal_target;
         }
 
-        match task.task_type() {
-            TaskType::Debug(_) => {
-                let Some(config) = task.resolved_debug_adapter_config() else {
-                    return;
-                };
-                let config = config.definition;
-
-                match &config.request {
-                    DebugRequest::Attach(attach_config) if attach_config.process_id.is_none() => {
-                        cx.emit(ShowAttachModal {
-                            debug_config: config.clone(),
-                        });
-                        return;
-                    }
-                    _ => {
-                        self.workspace
-                            .update(cx, |workspace, cx| {
-                                workspace.schedule_debug_task(task, window, cx);
-                            })
-                            .ok();
-                    }
-                }
-            }
-            TaskType::Script => {
-                self.workspace
-                    .update(cx, |workspace, cx| {
-                        workspace.schedule_resolved_task(
-                            task_source_kind,
-                            task,
-                            omit_history_entry,
-                            window,
-                            cx,
-                        );
-                    })
-                    .ok();
-            }
-        };
+        self.workspace
+            .update(cx, |workspace, cx| {
+                workspace.schedule_resolved_task(
+                    task_source_kind,
+                    task,
+                    omit_history_entry,
+                    window,
+                    cx,
+                );
+            })
+            .ok();
 
         cx.emit(DismissEvent);
     }
@@ -422,16 +391,14 @@ impl PickerDelegate for TasksModalDelegate {
         } else {
             String::new()
         };
-        if let Some(resolved) = resolved_task.resolved.as_ref() {
-            if resolved.command_label != display_label
-                && resolved.command_label != resolved_task.resolved_label
-            {
-                if !tooltip_label_text.trim().is_empty() {
-                    tooltip_label_text.push('\n');
-                }
-                tooltip_label_text.push_str(&resolved.command_label);
+
+        if resolved_task.resolved.command_label != resolved_task.resolved_label {
+            if !tooltip_label_text.trim().is_empty() {
+                tooltip_label_text.push('\n');
             }
+            tooltip_label_text.push_str(&resolved_task.resolved.command_label);
         }
+
         if template.tags.len() > 0 {
             tooltip_label_text.push('\n');
             tooltip_label_text.push_str(
@@ -553,7 +520,7 @@ impl PickerDelegate for TasksModalDelegate {
         let task_index = self.matches.get(self.selected_index())?.candidate_id;
         let tasks = self.candidates.as_ref()?;
         let (_, task) = tasks.get(task_index)?;
-        Some(task.resolved.as_ref()?.command_label.clone())
+        Some(task.resolved.command_label.clone())
     }
 
     fn confirm_input(
@@ -570,26 +537,17 @@ impl PickerDelegate for TasksModalDelegate {
             reveal_target: Some(reveal_target),
         }) = self.task_overrides
         {
-            if let Some(resolved_task) = &mut task.resolved {
-                resolved_task.reveal_target = reveal_target;
-            }
+            task.resolved.reveal_target = reveal_target;
         }
         self.workspace
             .update(cx, |workspace, cx| {
-                match task.task_type() {
-                    TaskType::Script => workspace.schedule_resolved_task(
-                        task_source_kind,
-                        task,
-                        omit_history_entry,
-                        window,
-                        cx,
-                    ),
-                    // 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.schedule_debug_task(task, window, cx);
-                    }
-                };
+                workspace.schedule_resolved_task(
+                    task_source_kind,
+                    task,
+                    omit_history_entry,
+                    window,
+                    cx,
+                )
             })
             .ok();
         cx.emit(DismissEvent);
@@ -716,10 +674,7 @@ fn string_match_candidates<'a>(
     candidates
         .into_iter()
         .enumerate()
-        .filter(|(_, (_, candidate))| match candidate.task_type() {
-            TaskType::Script => task_modal_type == TaskModal::ScriptModal,
-            TaskType::Debug(_) => task_modal_type == TaskModal::DebugModal,
-        })
+        .filter(|(_, (_, _))| task_modal_type == TaskModal::ScriptModal)
         .map(|(index, (_, candidate))| StringMatchCandidate::new(index, candidate.display_label()))
         .collect()
 }

crates/tasks_ui/src/tasks_ui.rs 🔗

@@ -65,13 +65,13 @@ pub fn init(cx: &mut App) {
                             })
                             .detach()
                         } else {
-                            if let Some(resolved) = last_scheduled_task.resolved.as_mut() {
-                                if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
-                                    resolved.allow_concurrent_runs = allow_concurrent_runs;
-                                }
-                                if let Some(use_new_terminal) = action.use_new_terminal {
-                                    resolved.use_new_terminal = use_new_terminal;
-                                }
+                            let resolved = &mut last_scheduled_task.resolved;
+
+                            if let Some(allow_concurrent_runs) = action.allow_concurrent_runs {
+                                resolved.allow_concurrent_runs = allow_concurrent_runs;
+                            }
+                            if let Some(use_new_terminal) = action.use_new_terminal {
+                                resolved.use_new_terminal = use_new_terminal;
                             }
 
                             workspace.schedule_resolved_task(

crates/workspace/src/tasks.rs 🔗

@@ -1,10 +1,11 @@
 use std::process::ExitStatus;
 
 use anyhow::{Result, anyhow};
-use gpui::{Context, Task};
+use gpui::{Context, Entity, Task};
+use language::Buffer;
 use project::TaskSourceKind;
 use remote::ConnectionState;
-use task::{DebugTaskDefinition, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate};
+use task::{DebugScenario, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate};
 use ui::Window;
 
 use crate::Workspace;
@@ -48,84 +49,41 @@ impl Workspace {
     pub fn schedule_resolved_task(
         self: &mut Workspace,
         task_source_kind: TaskSourceKind,
-        mut resolved_task: ResolvedTask,
+        resolved_task: ResolvedTask,
         omit_history: bool,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) {
-        if let Some(spawn_in_terminal) = resolved_task.resolved.take() {
-            if !omit_history {
-                resolved_task.resolved = Some(spawn_in_terminal.clone());
-                self.project().update(cx, |project, cx| {
-                    if let Some(task_inventory) =
-                        project.task_store().read(cx).task_inventory().cloned()
-                    {
-                        task_inventory.update(cx, |inventory, _| {
-                            inventory.task_scheduled(task_source_kind, resolved_task);
-                        })
-                    }
-                });
-            }
-
-            if let Some(terminal_provider) = self.terminal_provider.as_ref() {
-                terminal_provider
-                    .spawn(spawn_in_terminal, window, cx)
-                    .detach_and_log_err(cx);
-            }
-        }
-    }
-
-    pub fn schedule_debug_task(
-        &mut self,
-        task: ResolvedTask,
-        window: &mut Window,
-        cx: &mut Context<Workspace>,
-    ) {
-        let Some(debug_config) = task.resolved_debug_adapter_config() else {
-            log::error!("Debug task has no debug adapter config");
-            return;
-        };
-
-        let project = self.project().clone();
-        cx.spawn_in(window, async move |workspace, cx| {
-            let config = if debug_config.locator.is_some() {
-                let task = workspace.update_in(cx, |workspace, window, cx| {
-                    workspace.spawn_in_terminal(task.resolved.unwrap(), window, cx)
-                })?;
-
-                let exit_code = task.await?;
-                if !exit_code.success() {
-                    return anyhow::Ok(());
+        let spawn_in_terminal = resolved_task.resolved.clone();
+        if !omit_history {
+            self.project().update(cx, |project, cx| {
+                if let Some(task_inventory) =
+                    project.task_store().read(cx).task_inventory().cloned()
+                {
+                    task_inventory.update(cx, |inventory, _| {
+                        inventory.task_scheduled(task_source_kind, resolved_task);
+                    })
                 }
-                let ret = project
-                    .update(cx, |project, cx| {
-                        project.dap_store().update(cx, |dap_store, cx| {
-                            dap_store.run_debug_locator(debug_config, cx)
-                        })
-                    })?
-                    .await?;
-                ret
-            } else {
-                debug_config.definition
-            };
-
-            workspace.update_in(cx, |workspace, window, cx| {
-                workspace.start_debug_session(config, window, cx);
-            })?;
+            });
+        }
 
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
+        if let Some(terminal_provider) = self.terminal_provider.as_ref() {
+            terminal_provider
+                .spawn(spawn_in_terminal, window, cx)
+                .detach_and_log_err(cx);
+        }
     }
 
     pub fn start_debug_session(
         &mut self,
-        definition: DebugTaskDefinition,
+        scenario: DebugScenario,
+        task_context: TaskContext,
+        active_buffer: Option<Entity<Buffer>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         if let Some(provider) = self.debugger_provider.as_mut() {
-            provider.start_session(definition, window, cx)
+            provider.start_session(scenario, task_context, active_buffer, window, cx)
         }
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -49,7 +49,7 @@ pub use item::{
     ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
 };
 use itertools::Itertools;
-use language::{LanguageRegistry, Rope};
+use language::{Buffer, LanguageRegistry, Rope};
 pub use modal_layer::*;
 use node_runtime::NodeRuntime;
 use notifications::{
@@ -96,7 +96,7 @@ use std::{
     sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
     time::Duration,
 };
-use task::{DebugTaskDefinition, SpawnInTerminal};
+use task::{DebugScenario, SpawnInTerminal, TaskContext};
 use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
 pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
 pub use ui;
@@ -140,7 +140,15 @@ pub trait TerminalProvider {
 }
 
 pub trait DebuggerProvider {
-    fn start_session(&self, definition: DebugTaskDefinition, window: &mut Window, cx: &mut App);
+    // `active_buffer` is used to resolve build task's name against language-specific tasks.
+    fn start_session(
+        &self,
+        definition: DebugScenario,
+        task_context: TaskContext,
+        active_buffer: Option<Entity<Buffer>>,
+        window: &mut Window,
+        cx: &mut App,
+    );
 }
 
 actions!(

crates/zed/src/zed.rs 🔗

@@ -4273,7 +4273,7 @@ mod tests {
             project::debugger::breakpoint_store::BreakpointStore::init(
                 &app_state.client.clone().into(),
             );
-            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into());
+            project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx);
             debugger_ui::init(cx);
             initialize_workspace(app_state.clone(), prompt_builder, cx);
             search::init(cx);