debugger: Run build in terminal (#29645)

Conrad Irwin and Piotr Osiewicz created

Currently contains the pre-work of making sessions creatable without a
definition, but still need to change the spawn in terminal
to use the running session

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>

Change summary

crates/dap/src/adapters.rs                     |  10 
crates/debugger_tools/src/dap_log.rs           |   4 
crates/debugger_ui/src/debugger_panel.rs       | 324 +++------
crates/debugger_ui/src/new_session_modal.rs    | 653 +++++++++----------
crates/debugger_ui/src/persistence.rs          |   4 
crates/debugger_ui/src/session/running.rs      | 148 ++++
crates/debugger_ui/src/tests/debugger_panel.rs |  10 
crates/project/src/debugger/dap_store.rs       |  16 
crates/project/src/debugger/session.rs         |  51 
crates/project/src/project.rs                  |   9 
crates/task/src/lib.rs                         |   3 
crates/task/src/task_template.rs               |  47 +
12 files changed, 676 insertions(+), 603 deletions(-)

Detailed changes

crates/dap/src/adapters.rs 🔗

@@ -88,7 +88,7 @@ impl<'a> From<&'a str> for DebugAdapterName {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct TcpArguments {
     pub host: Ipv4Addr,
     pub port: u16,
@@ -127,7 +127,7 @@ impl TcpArguments {
 )]
 pub struct DebugTaskDefinition {
     pub label: SharedString,
-    pub adapter: SharedString,
+    pub adapter: DebugAdapterName,
     pub request: DebugRequest,
     /// Additional initialization arguments to be sent on DAP initialization
     pub initialize_args: Option<serde_json::Value>,
@@ -153,7 +153,7 @@ impl DebugTaskDefinition {
     pub fn to_scenario(&self) -> DebugScenario {
         DebugScenario {
             label: self.label.clone(),
-            adapter: self.adapter.clone(),
+            adapter: self.adapter.clone().into(),
             build: None,
             request: Some(self.request.clone()),
             stop_on_entry: self.stop_on_entry,
@@ -207,7 +207,7 @@ impl DebugTaskDefinition {
                 .map(TcpArgumentsTemplate::from_proto)
                 .transpose()?,
             stop_on_entry: proto.stop_on_entry,
-            adapter: proto.adapter.into(),
+            adapter: DebugAdapterName(proto.adapter.into()),
             request: match request {
                 proto::debug_task_definition::Request::DebugAttachRequest(config) => {
                     DebugRequest::Attach(AttachRequest {
@@ -229,7 +229,7 @@ impl DebugTaskDefinition {
 }
 
 /// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub struct DebugAdapterBinary {
     pub command: String,
     pub arguments: Vec<String>,

crates/debugger_tools/src/dap_log.rs 🔗

@@ -568,11 +568,11 @@ impl DapLogView {
             .sessions()
             .filter_map(|session| {
                 let session = session.read(cx);
-                session.adapter_name();
+                session.adapter();
                 let client = session.adapter_client()?;
                 Some(DapMenuItem {
                     client_id: client.id(),
-                    client_name: session.adapter_name().to_string(),
+                    client_name: session.adapter().to_string(),
                     has_adapter_logs: client.has_adapter_logs(),
                     selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
                 })

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -6,10 +6,9 @@ use crate::{
     persistence,
 };
 use crate::{new_session_modal::NewSessionModal, session::DebugSession};
-use anyhow::{Context as _, Result, anyhow};
-use collections::{HashMap, HashSet};
+use anyhow::Result;
 use command_palette_hooks::CommandPaletteFilter;
-use dap::DebugRequest;
+use dap::adapters::DebugAdapterName;
 use dap::{
     ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
     client::SessionId, debugger_settings::DebuggerSettings,
@@ -27,7 +26,6 @@ use project::{Project, debugger::session::ThreadStatus};
 use rpc::proto::{self};
 use settings::Settings;
 use std::any::TypeId;
-use std::path::PathBuf;
 use task::{DebugScenario, TaskContext};
 use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 use workspace::SplitDirection;
@@ -200,59 +198,65 @@ impl DebugPanel {
         })
     }
 
-    fn start_from_definition(
+    pub fn start_session(
         &mut self,
-        definition: DebugTaskDefinition,
+        scenario: DebugScenario,
+        task_context: TaskContext,
+        active_buffer: Option<Entity<Buffer>>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        cx.spawn_in(window, async move |this, cx| {
-            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 session = dap_store.new_session(definition, None, cx);
-
-                (session.clone(), dap_store.boot_session(session, cx))
-            })?;
-            Self::register_session(this.clone(), session.clone(), cx).await?;
-
-            if let Err(e) = task.await {
-                this.update(cx, |this, cx| {
-                    this.workspace
-                        .update(cx, |workspace, cx| {
-                            workspace.show_error(&e, cx);
+    ) {
+        let dap_store = self.project.read(cx).dap_store();
+        let workspace = self.workspace.clone();
+        let session = dap_store.update(cx, |dap_store, cx| {
+            dap_store.new_session(
+                scenario.label.clone(),
+                DebugAdapterName(scenario.adapter.clone()),
+                None,
+                cx,
+            )
+        });
+        let task = cx.spawn_in(window, {
+            let session = session.clone();
+            async move |this, cx| {
+                let debug_session =
+                    Self::register_session(this.clone(), session.clone(), cx).await?;
+                let definition = debug_session
+                    .update_in(cx, |debug_session, window, cx| {
+                        debug_session.running_state().update(cx, |running, cx| {
+                            running.resolve_scenario(
+                                scenario,
+                                task_context,
+                                active_buffer,
+                                window,
+                                cx,
+                            )
                         })
-                        .ok();
-                })
-                .ok();
+                    })?
+                    .await?;
 
+                dap_store
+                    .update(cx, |dap_store, cx| {
+                        dap_store.boot_session(session.clone(), definition, cx)
+                    })?
+                    .await
+            }
+        });
+
+        cx.spawn(async move |_, cx| {
+            if let Err(error) = task.await {
+                log::error!("{:?}", error);
+                workspace
+                    .update(cx, |workspace, cx| {
+                        workspace.show_error(&error, cx);
+                    })
+                    .ok();
                 session
                     .update(cx, |session, cx| session.shutdown(cx))?
                     .await;
             }
-
             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);
     }
 
@@ -260,33 +264,15 @@ impl DebugPanel {
         this: WeakEntity<Self>,
         session: Entity<Session>,
         cx: &mut AsyncWindowContext,
-    ) -> Result<()> {
-        let adapter_name = session.update(cx, |session, _| session.adapter_name())?;
+    ) -> Result<Entity<DebugSession>> {
+        let adapter_name = session.update(cx, |session, _| session.adapter())?;
         this.update_in(cx, |_, window, cx| {
             cx.subscribe_in(
                 &session,
                 window,
                 move |this, session, event: &SessionStateEvent, window, cx| match event {
                     SessionStateEvent::Restart => {
-                        let mut curr_session = session.clone();
-                        while let Some(parent_session) = curr_session
-                            .read_with(cx, |session, _| session.parent_session().cloned())
-                        {
-                            curr_session = parent_session;
-                        }
-
-                        let definition = curr_session.update(cx, |session, _| session.definition());
-                        let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
-
-                        cx.spawn_in(window, async move |this, cx| {
-                            task.await;
-
-                            this.update_in(cx, |this, window, cx| {
-                                this.start_from_definition(definition, window, cx)
-                            })?
-                            .await
-                        })
-                        .detach_and_log_err(cx);
+                        this.handle_restart_request(session.clone(), window, cx);
                     }
                     SessionStateEvent::SpawnChildSession { request } => {
                         this.handle_start_debugging_request(request, session.clone(), window, cx);
@@ -300,7 +286,7 @@ impl DebugPanel {
 
         let serialized_layout = persistence::get_serialized_pane_layout(adapter_name).await;
 
-        let workspace = this.update_in(cx, |this, window, cx| {
+        let (debug_session, workspace) = this.update_in(cx, |this, window, cx| {
             this.sessions.retain(|session| {
                 session
                     .read(cx)
@@ -311,7 +297,7 @@ impl DebugPanel {
                     .is_terminated()
             });
 
-            let session_item = DebugSession::running(
+            let debug_session = DebugSession::running(
                 this.project.clone(),
                 this.workspace.clone(),
                 session,
@@ -324,20 +310,62 @@ impl DebugPanel {
             // We might want to make this an event subscription and only notify when a new thread is selected
             // This is used to filter the command menu correctly
             cx.observe(
-                &session_item.read(cx).running_state().clone(),
+                &debug_session.read(cx).running_state().clone(),
                 |_, _, cx| cx.notify(),
             )
             .detach();
 
-            this.sessions.push(session_item.clone());
-            this.activate_session(session_item, window, cx);
-            this.workspace.clone()
+            this.sessions.push(debug_session.clone());
+            this.activate_session(debug_session.clone(), window, cx);
+
+            (debug_session, this.workspace.clone())
         })?;
 
         workspace.update_in(cx, |workspace, window, cx| {
             workspace.focus_panel::<Self>(window, cx);
         })?;
-        Ok(())
+
+        Ok(debug_session)
+    }
+
+    fn handle_restart_request(
+        &mut self,
+        mut curr_session: Entity<Session>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        while let Some(parent_session) =
+            curr_session.read_with(cx, |session, _| session.parent_session().cloned())
+        {
+            curr_session = parent_session;
+        }
+
+        let Some(worktree) = curr_session.read(cx).worktree() else {
+            log::error!("Attempted to start a child session from non local debug session");
+            return;
+        };
+
+        let dap_store_handle = self.project.read(cx).dap_store().clone();
+        let label = curr_session.read(cx).label().clone();
+        let adapter = curr_session.read(cx).adapter().clone();
+        let binary = curr_session.read(cx).binary().clone();
+        let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
+
+        cx.spawn_in(window, async move |this, cx| {
+            task.await;
+
+            let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
+                let session = dap_store.new_session(label, adapter, None, cx);
+
+                let task = session.update(cx, |session, cx| {
+                    session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
+                });
+                (session, task)
+            })?;
+            Self::register_session(this, session, cx).await?;
+            task.await
+        })
+        .detach_and_log_err(cx);
     }
 
     pub fn handle_start_debugging_request(
@@ -353,40 +381,23 @@ impl DebugPanel {
         };
 
         let dap_store_handle = self.project.read(cx).dap_store().clone();
-        let definition = parent_session.read(cx).definition().clone();
+        let label = parent_session.read(cx).label().clone();
+        let adapter = parent_session.read(cx).adapter().clone();
         let mut binary = parent_session.read(cx).binary().clone();
         binary.request_args = request.clone();
 
         cx.spawn_in(window, async move |this, cx| {
             let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
                 let session =
-                    dap_store.new_session(definition.clone(), Some(parent_session.clone()), cx);
+                    dap_store.new_session(label, adapter, Some(parent_session.clone()), cx);
 
                 let task = session.update(cx, |session, cx| {
                     session.boot(binary, worktree, dap_store_handle.downgrade(), cx)
                 });
                 (session, task)
             })?;
-
-            match task.await {
-                Err(e) => {
-                    this.update(cx, |this, cx| {
-                        this.workspace
-                            .update(cx, |workspace, cx| {
-                                workspace.show_error(&e, cx);
-                            })
-                            .ok();
-                    })
-                    .ok();
-
-                    session
-                        .update(cx, |session, cx| session.shutdown(cx))?
-                        .await;
-                }
-                Ok(_) => Self::register_session(this, session, cx).await?,
-            }
-
-            anyhow::Ok(())
+            Self::register_session(this, session, cx).await?;
+            task.await
         })
         .detach_and_log_err(cx);
     }
@@ -394,127 +405,6 @@ impl DebugPanel {
     pub fn active_session(&self) -> Option<Entity<DebugSession>> {
         self.active_session.clone()
     }
-
-    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 {
-                if let DebugRequest::Launch(launch_config) = &mut request {
-                    let mut variable_names = HashMap::default();
-                    let mut substituted_variables = HashSet::default();
-                    let task_variables = task_context
-                        .task_variables
-                        .iter()
-                        .map(|(key, value)| {
-                            let key_string = key.to_string();
-                            if !variable_names.contains_key(&key_string) {
-                                variable_names.insert(key_string.clone(), key.clone());
-                            }
-                            (key_string, value.as_str())
-                        })
-                        .collect::<HashMap<_, _>>();
-
-                    let cwd = launch_config
-                        .cwd
-                        .as_ref()
-                        .and_then(|cwd| cwd.to_str())
-                        .and_then(|cwd| {
-                            task::substitute_all_template_variables_in_str(
-                                cwd,
-                                &task_variables,
-                                &variable_names,
-                                &mut substituted_variables,
-                            )
-                        });
-
-                    if let Some(cwd) = cwd {
-                        launch_config.cwd = Some(PathBuf::from(cwd))
-                    }
-
-                    if let Some(program) = task::substitute_all_template_variables_in_str(
-                        &launch_config.program,
-                        &task_variables,
-                        &variable_names,
-                        &mut substituted_variables,
-                    ) {
-                        launch_config.program = program;
-                    }
-
-                    for arg in launch_config.args.iter_mut() {
-                        if let Some(substituted_arg) =
-                            task::substitute_all_template_variables_in_str(
-                                &arg,
-                                &task_variables,
-                                &variable_names,
-                                &mut substituted_variables,
-                            )
-                        {
-                            *arg = substituted_arg;
-                        }
-                    }
-                }
-
-                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.transpose()?.context("task cancelled")?;
-                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 close_session(&mut self, entity_id: EntityId, window: &mut Window, cx: &mut Context<Self>) {
         let Some(session) = self
             .sessions

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -4,7 +4,10 @@ use std::{
     path::{Path, PathBuf},
 };
 
-use dap::{DapRegistry, DebugRequest, adapters::DebugTaskDefinition};
+use dap::{
+    DapRegistry, DebugRequest,
+    adapters::{DebugAdapterName, DebugTaskDefinition},
+};
 use editor::{Editor, EditorElement, EditorStyle};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
@@ -13,9 +16,9 @@ use gpui::{
 };
 use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
 use project::{TaskSourceKind, task_store::TaskStore};
-use session_modes::{AttachMode, DebugScenarioDelegate, LaunchMode};
 use settings::Settings;
 use task::{DebugScenario, LaunchRequest};
+use tasks_ui::task_contexts;
 use theme::ThemeSettings;
 use ui::{
     ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
@@ -36,7 +39,7 @@ pub(super) struct NewSessionModal {
     mode: NewSessionMode,
     stop_on_entry: ToggleState,
     initialize_args: Option<serde_json::Value>,
-    debugger: Option<SharedString>,
+    debugger: Option<DebugAdapterName>,
     last_selected_profile_name: Option<SharedString>,
 }
 
@@ -143,16 +146,19 @@ impl NewSessionModal {
 
         let debug_panel = self.debug_panel.clone();
         let workspace = self.workspace.clone();
-
         cx.spawn_in(window, async move |this, cx| {
             let task_contexts = workspace
-                .update_in(cx, |workspace, window, cx| {
-                    tasks_ui::task_contexts(workspace, window, cx)
-                })?
+                .update_in(cx, |this, window, cx| task_contexts(this, window, cx))?
                 .await;
-
-            let task_context = task_contexts.active_context().cloned().unwrap_or_default();
-
+            let task_context = task_contexts
+                .active_item_context
+                .map(|(_, _, context)| context)
+                .or_else(|| {
+                    task_contexts
+                        .active_worktree_context
+                        .map(|(_, context)| context)
+                })
+                .unwrap_or_default();
             debug_panel.update_in(cx, |debug_panel, window, cx| {
                 debug_panel.start_session(config, task_context, None, window, cx)
             })?;
@@ -167,18 +173,17 @@ impl NewSessionModal {
 
     fn update_attach_picker(
         attach: &Entity<AttachMode>,
-        selected_debugger: &str,
+        adapter: &DebugAdapterName,
         window: &mut Window,
         cx: &mut App,
     ) {
         attach.update(cx, |this, cx| {
-            if selected_debugger != this.definition.adapter.as_ref() {
-                let adapter: SharedString = selected_debugger.to_owned().into();
+            if adapter != &this.definition.adapter {
                 this.definition.adapter = adapter.clone();
 
                 this.attach_picker.update(cx, |this, cx| {
                     this.picker.update(cx, |this, cx| {
-                        this.delegate.definition.adapter = adapter;
+                        this.delegate.definition.adapter = adapter.clone();
                         this.focus(window, cx);
                     })
                 });
@@ -194,15 +199,16 @@ impl NewSessionModal {
     ) -> ui::DropdownMenu {
         let workspace = self.workspace.clone();
         let weak = cx.weak_entity();
-        let debugger = self.debugger.clone();
+        let label = self
+            .debugger
+            .as_ref()
+            .map(|d| d.0.clone())
+            .unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
         DropdownMenu::new(
             "dap-adapter-picker",
-            debugger
-                .as_ref()
-                .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
-                .clone(),
+            label,
             ContextMenu::build(window, cx, move |mut menu, _, cx| {
-                let setter_for_name = |name: SharedString| {
+                let setter_for_name = |name: DebugAdapterName| {
                     let weak = weak.clone();
                     move |window: &mut Window, cx: &mut App| {
                         weak.update(cx, |this, cx| {
@@ -222,7 +228,7 @@ impl NewSessionModal {
                     .unwrap_or_default();
 
                 for adapter in available_adapters {
-                    menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.0.clone()));
+                    menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
                 }
                 menu
             }),
@@ -251,7 +257,7 @@ impl NewSessionModal {
                     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());
+                            this.debugger = Some(DebugAdapterName(task.adapter.clone()));
                             this.initialize_args = task.initialize_args.clone();
                             match &task.request {
                                 Some(DebugRequest::Launch(launch_config)) => {
@@ -374,7 +380,7 @@ impl NewSessionMode {
     }
 
     fn attach(
-        debugger: Option<SharedString>,
+        debugger: Option<DebugAdapterName>,
         workspace: Entity<Workspace>,
         window: &mut Window,
         cx: &mut Context<NewSessionModal>,
@@ -431,41 +437,6 @@ impl Focusable for NewSessionMode {
     }
 }
 
-impl RenderOnce for LaunchMode {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
-        v_flex()
-            .p_2()
-            .w_full()
-            .gap_3()
-            .track_focus(&self.program.focus_handle(cx))
-            .child(
-                div().child(
-                    Label::new("Program")
-                        .size(ui::LabelSize::Small)
-                        .color(Color::Muted),
-                ),
-            )
-            .child(render_editor(&self.program, window, cx))
-            .child(
-                div().child(
-                    Label::new("Working Directory")
-                        .size(ui::LabelSize::Small)
-                        .color(Color::Muted),
-                ),
-            )
-            .child(render_editor(&self.cwd, window, cx))
-    }
-}
-
-impl RenderOnce for AttachMode {
-    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
-        v_flex()
-            .w_full()
-            .track_focus(&self.attach_picker.focus_handle(cx))
-            .child(self.attach_picker.clone())
-    }
-}
-
 impl RenderOnce for NewSessionMode {
     fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
         match self {
@@ -684,318 +655,342 @@ impl Focusable for NewSessionModal {
 
 impl ModalView for NewSessionModal {}
 
-// This module makes sure that the modes setup the correct subscriptions whenever they're created
-mod session_modes {
-    use std::rc::Rc;
-
-    use super::*;
+impl RenderOnce for LaunchMode {
+    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+        v_flex()
+            .p_2()
+            .w_full()
+            .gap_3()
+            .track_focus(&self.program.focus_handle(cx))
+            .child(
+                div().child(
+                    Label::new("Program")
+                        .size(ui::LabelSize::Small)
+                        .color(Color::Muted),
+                ),
+            )
+            .child(render_editor(&self.program, window, cx))
+            .child(
+                div().child(
+                    Label::new("Working Directory")
+                        .size(ui::LabelSize::Small)
+                        .color(Color::Muted),
+                ),
+            )
+            .child(render_editor(&self.cwd, window, cx))
+    }
+}
 
-    #[derive(Clone)]
-    #[non_exhaustive]
-    pub(super) struct LaunchMode {
-        pub(super) program: Entity<Editor>,
-        pub(super) cwd: Entity<Editor>,
+impl RenderOnce for AttachMode {
+    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
+        v_flex()
+            .w_full()
+            .track_focus(&self.attach_picker.focus_handle(cx))
+            .child(self.attach_picker.clone())
     }
+}
+
+use std::rc::Rc;
 
-    impl LaunchMode {
-        pub(super) fn new(
-            past_launch_config: Option<LaunchRequest>,
-            window: &mut Window,
-            cx: &mut App,
-        ) -> Entity<Self> {
-            let (past_program, past_cwd) = past_launch_config
-                .map(|config| (Some(config.program), config.cwd))
-                .unwrap_or_else(|| (None, None));
+#[derive(Clone)]
+pub(super) struct LaunchMode {
+    program: Entity<Editor>,
+    cwd: Entity<Editor>,
+}
 
-            let program = cx.new(|cx| Editor::single_line(window, cx));
-            program.update(cx, |this, cx| {
-                this.set_placeholder_text("Program path", cx);
+impl LaunchMode {
+    pub(super) fn new(
+        past_launch_config: Option<LaunchRequest>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Entity<Self> {
+        let (past_program, past_cwd) = past_launch_config
+            .map(|config| (Some(config.program), config.cwd))
+            .unwrap_or_else(|| (None, None));
 
-                if let Some(past_program) = past_program {
-                    this.set_text(past_program, window, cx);
-                };
-            });
-            let cwd = cx.new(|cx| Editor::single_line(window, cx));
-            cwd.update(cx, |this, cx| {
-                this.set_placeholder_text("Working Directory", cx);
-                if let Some(past_cwd) = past_cwd {
-                    this.set_text(past_cwd.to_string_lossy(), window, cx);
-                };
-            });
-            cx.new(|_| Self { program, cwd })
-        }
+        let program = cx.new(|cx| Editor::single_line(window, cx));
+        program.update(cx, |this, cx| {
+            this.set_placeholder_text("Program path", cx);
 
-        pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
-            let path = self.cwd.read(cx).text(cx);
-            task::LaunchRequest {
-                program: self.program.read(cx).text(cx),
-                cwd: path.is_empty().not().then(|| PathBuf::from(path)),
-                args: Default::default(),
-                env: Default::default(),
-            }
+            if let Some(past_program) = past_program {
+                this.set_text(past_program, window, cx);
+            };
+        });
+        let cwd = cx.new(|cx| Editor::single_line(window, cx));
+        cwd.update(cx, |this, cx| {
+            this.set_placeholder_text("Working Directory", cx);
+            if let Some(past_cwd) = past_cwd {
+                this.set_text(past_cwd.to_string_lossy(), window, cx);
+            };
+        });
+        cx.new(|_| Self { program, cwd })
+    }
+
+    pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
+        let path = self.cwd.read(cx).text(cx);
+        task::LaunchRequest {
+            program: self.program.read(cx).text(cx),
+            cwd: path.is_empty().not().then(|| PathBuf::from(path)),
+            args: Default::default(),
+            env: Default::default(),
         }
     }
+}
 
-    #[derive(Clone)]
-    pub(super) struct AttachMode {
-        pub(super) definition: DebugTaskDefinition,
-        pub(super) attach_picker: Entity<AttachModal>,
-        _subscription: Rc<Subscription>,
-    }
-
-    impl AttachMode {
-        pub(super) fn new(
-            debugger: Option<SharedString>,
-            workspace: Entity<Workspace>,
-            window: &mut Window,
-            cx: &mut Context<NewSessionModal>,
-        ) -> Entity<Self> {
-            let definition = DebugTaskDefinition {
-                adapter: debugger.clone().unwrap_or_default(),
-                label: "Attach New Session Setup".into(),
-                request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
-                initialize_args: None,
-                tcp_connection: None,
-                stop_on_entry: Some(false),
-            };
-            let attach_picker = cx.new(|cx| {
-                let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
-                window.focus(&modal.focus_handle(cx));
+#[derive(Clone)]
+pub(super) struct AttachMode {
+    pub(super) definition: DebugTaskDefinition,
+    pub(super) attach_picker: Entity<AttachModal>,
+    _subscription: Rc<Subscription>,
+}
 
-                modal
-            });
+impl AttachMode {
+    pub(super) fn new(
+        debugger: Option<DebugAdapterName>,
+        workspace: Entity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<NewSessionModal>,
+    ) -> Entity<Self> {
+        let definition = DebugTaskDefinition {
+            adapter: debugger.unwrap_or(DebugAdapterName("".into())),
+            label: "Attach New Session Setup".into(),
+            request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
+            initialize_args: None,
+            tcp_connection: None,
+            stop_on_entry: Some(false),
+        };
+        let attach_picker = cx.new(|cx| {
+            let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
+            window.focus(&modal.focus_handle(cx));
 
-            let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
-                cx.emit(DismissEvent);
-            });
+            modal
+        });
 
-            cx.new(|_| Self {
-                definition,
-                attach_picker,
-                _subscription: Rc::new(subscription),
-            })
-        }
-        pub(super) fn debug_task(&self) -> task::AttachRequest {
-            task::AttachRequest { process_id: None }
-        }
+        let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
+            cx.emit(DismissEvent);
+        });
+
+        cx.new(|_| Self {
+            definition,
+            attach_picker,
+            _subscription: Rc::new(subscription),
+        })
     }
+    pub(super) fn debug_task(&self) -> task::AttachRequest {
+        task::AttachRequest { process_id: None }
+    }
+}
 
-    pub(super) struct DebugScenarioDelegate {
-        task_store: Entity<TaskStore>,
-        candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
-        selected_index: usize,
-        matches: Vec<StringMatch>,
-        prompt: String,
+pub(super) struct DebugScenarioDelegate {
+    task_store: Entity<TaskStore>,
+    candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
+    selected_index: usize,
+    matches: Vec<StringMatch>,
+    prompt: String,
+    debug_panel: WeakEntity<DebugPanel>,
+    workspace: WeakEntity<Workspace>,
+}
+
+impl DebugScenarioDelegate {
+    pub(super) fn new(
         debug_panel: WeakEntity<DebugPanel>,
         workspace: WeakEntity<Workspace>,
+        task_store: Entity<TaskStore>,
+    ) -> Self {
+        Self {
+            task_store,
+            candidates: None,
+            selected_index: 0,
+            matches: Vec::new(),
+            prompt: String::new(),
+            debug_panel,
+            workspace,
+        }
     }
+}
 
-    impl DebugScenarioDelegate {
-        pub(super) fn new(
-            debug_panel: WeakEntity<DebugPanel>,
-            workspace: WeakEntity<Workspace>,
-            task_store: Entity<TaskStore>,
-        ) -> Self {
-            Self {
-                task_store,
-                candidates: None,
-                selected_index: 0,
-                matches: Vec::new(),
-                prompt: String::new(),
-                debug_panel,
-                workspace,
-            }
-        }
+impl PickerDelegate for DebugScenarioDelegate {
+    type ListItem = ui::ListItem;
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
     }
 
-    impl PickerDelegate for DebugScenarioDelegate {
-        type ListItem = ui::ListItem;
+    fn selected_index(&self) -> usize {
+        self.selected_index
+    }
 
-        fn match_count(&self) -> usize {
-            self.matches.len()
-        }
+    fn set_selected_index(
+        &mut self,
+        ix: usize,
+        _window: &mut Window,
+        _cx: &mut Context<picker::Picker<Self>>,
+    ) {
+        self.selected_index = ix;
+    }
 
-        fn selected_index(&self) -> usize {
-            self.selected_index
-        }
+    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
+        "".into()
+    }
 
-        fn set_selected_index(
-            &mut self,
-            ix: usize,
-            _window: &mut Window,
-            _cx: &mut Context<picker::Picker<Self>>,
-        ) {
-            self.selected_index = ix;
-        }
+    fn update_matches(
+        &mut self,
+        query: String,
+        window: &mut Window,
+        cx: &mut Context<picker::Picker<Self>>,
+    ) -> gpui::Task<()> {
+        let candidates: Vec<_> = match &self.candidates {
+            Some(candidates) => candidates
+                .into_iter()
+                .enumerate()
+                .map(|(index, (_, candidate))| {
+                    StringMatchCandidate::new(index, candidate.label.as_ref())
+                })
+                .collect(),
+            None => {
+                let worktree_ids: Vec<_> = self
+                    .workspace
+                    .update(cx, |this, cx| {
+                        this.visible_worktrees(cx)
+                            .map(|tree| tree.read(cx).id())
+                            .collect()
+                    })
+                    .ok()
+                    .unwrap_or_default();
 
-        fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
-            "".into()
-        }
+                let scenarios: Vec<_> = self
+                    .task_store
+                    .read(cx)
+                    .task_inventory()
+                    .map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
+                    .unwrap_or_default();
 
-        fn update_matches(
-            &mut self,
-            query: String,
-            window: &mut Window,
-            cx: &mut Context<picker::Picker<Self>>,
-        ) -> gpui::Task<()> {
-            let candidates: Vec<_> = match &self.candidates {
-                Some(candidates) => candidates
+                self.candidates = Some(scenarios.clone());
+
+                scenarios
                     .into_iter()
                     .enumerate()
                     .map(|(index, (_, candidate))| {
                         StringMatchCandidate::new(index, candidate.label.as_ref())
                     })
-                    .collect(),
-                None => {
-                    let worktree_ids: Vec<_> = self
-                        .workspace
-                        .update(cx, |this, cx| {
-                            this.visible_worktrees(cx)
-                                .map(|tree| tree.read(cx).id())
-                                .collect()
-                        })
-                        .ok()
-                        .unwrap_or_default();
-
-                    let scenarios: Vec<_> = self
-                        .task_store
-                        .read(cx)
-                        .task_inventory()
-                        .map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
-                        .unwrap_or_default();
-
-                    self.candidates = Some(scenarios.clone());
-
-                    scenarios
-                        .into_iter()
-                        .enumerate()
-                        .map(|(index, (_, candidate))| {
-                            StringMatchCandidate::new(index, candidate.label.as_ref())
-                        })
-                        .collect()
-                }
-            };
+                    .collect()
+            }
+        };
 
-            cx.spawn_in(window, async move |picker, cx| {
-                let matches = fuzzy::match_strings(
-                    &candidates,
-                    &query,
-                    true,
-                    1000,
-                    &Default::default(),
-                    cx.background_executor().clone(),
-                )
-                .await;
+        cx.spawn_in(window, async move |picker, cx| {
+            let matches = fuzzy::match_strings(
+                &candidates,
+                &query,
+                true,
+                1000,
+                &Default::default(),
+                cx.background_executor().clone(),
+            )
+            .await;
 
-                picker
-                    .update(cx, |picker, _| {
-                        let delegate = &mut picker.delegate;
-
-                        delegate.matches = matches;
-                        delegate.prompt = query;
-
-                        if delegate.matches.is_empty() {
-                            delegate.selected_index = 0;
-                        } else {
-                            delegate.selected_index =
-                                delegate.selected_index.min(delegate.matches.len() - 1);
-                        }
-                    })
-                    .log_err();
-            })
-        }
+            picker
+                .update(cx, |picker, _| {
+                    let delegate = &mut picker.delegate;
 
-        fn confirm(
-            &mut self,
-            _: bool,
-            window: &mut Window,
-            cx: &mut Context<picker::Picker<Self>>,
-        ) {
-            let debug_scenario =
-                self.matches
-                    .get(self.selected_index())
-                    .and_then(|match_candidate| {
-                        self.candidates
-                            .as_ref()
-                            .map(|candidates| candidates[match_candidate.candidate_id].clone())
-                    });
-
-            let Some((task_source_kind, debug_scenario)) = debug_scenario else {
-                return;
-            };
+                    delegate.matches = matches;
+                    delegate.prompt = query;
 
-            let task_context = if let TaskSourceKind::Worktree {
-                id: worktree_id,
-                directory_in_worktree: _,
-                id_base: _,
-            } = task_source_kind
-            {
-                let workspace = self.workspace.clone();
-
-                cx.spawn_in(window, async move |_, cx| {
-                    workspace
-                        .update_in(cx, |workspace, window, cx| {
-                            tasks_ui::task_contexts(workspace, window, cx)
-                        })
-                        .ok()?
-                        .await
-                        .task_context_for_worktree_id(worktree_id)
-                        .cloned()
+                    if delegate.matches.is_empty() {
+                        delegate.selected_index = 0;
+                    } else {
+                        delegate.selected_index =
+                            delegate.selected_index.min(delegate.matches.len() - 1);
+                    }
                 })
-            } else {
-                gpui::Task::ready(None)
-            };
+                .log_err();
+        })
+    }
 
-            cx.spawn_in(window, async move |this, cx| {
-                let task_context = task_context.await.unwrap_or_default();
+    fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
+        let debug_scenario = self
+            .matches
+            .get(self.selected_index())
+            .and_then(|match_candidate| {
+                self.candidates
+                    .as_ref()
+                    .map(|candidates| candidates[match_candidate.candidate_id].clone())
+            });
 
-                this.update_in(cx, |this, window, cx| {
-                    this.delegate
-                        .debug_panel
-                        .update(cx, |panel, cx| {
-                            panel.start_session(debug_scenario, task_context, None, window, cx);
-                        })
-                        .ok();
+        let Some((task_source_kind, debug_scenario)) = debug_scenario else {
+            return;
+        };
 
-                    cx.emit(DismissEvent);
-                })
-                .ok();
+        let task_context = if let TaskSourceKind::Worktree {
+            id: worktree_id,
+            directory_in_worktree: _,
+            id_base: _,
+        } = task_source_kind
+        {
+            let workspace = self.workspace.clone();
+
+            cx.spawn_in(window, async move |_, cx| {
+                workspace
+                    .update_in(cx, |workspace, window, cx| {
+                        tasks_ui::task_contexts(workspace, window, cx)
+                    })
+                    .ok()?
+                    .await
+                    .task_context_for_worktree_id(worktree_id)
+                    .cloned()
             })
-            .detach();
-        }
+        } else {
+            gpui::Task::ready(None)
+        };
 
-        fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
-            cx.emit(DismissEvent);
-        }
+        cx.spawn_in(window, async move |this, cx| {
+            let task_context = task_context.await.unwrap_or_default();
 
-        fn render_match(
-            &self,
-            ix: usize,
-            selected: bool,
-            window: &mut Window,
-            cx: &mut Context<picker::Picker<Self>>,
-        ) -> Option<Self::ListItem> {
-            let hit = &self.matches[ix];
-
-            let highlighted_location = HighlightedMatch {
-                text: hit.string.clone(),
-                highlight_positions: hit.positions.clone(),
-                char_count: hit.string.chars().count(),
-                color: Color::Default,
-            };
+            this.update_in(cx, |this, window, cx| {
+                this.delegate
+                    .debug_panel
+                    .update(cx, |panel, cx| {
+                        panel.start_session(debug_scenario, task_context, None, window, cx);
+                    })
+                    .ok();
 
-            let icon = Icon::new(IconName::FileTree)
-                .color(Color::Muted)
-                .size(ui::IconSize::Small);
-
-            Some(
-                ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
-                    .inset(true)
-                    .start_slot::<Icon>(icon)
-                    .spacing(ListItemSpacing::Sparse)
-                    .toggle_state(selected)
-                    .child(highlighted_location.render(window, cx)),
-            )
-        }
+                cx.emit(DismissEvent);
+            })
+            .ok();
+        })
+        .detach();
+    }
+
+    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
+        cx.emit(DismissEvent);
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        window: &mut Window,
+        cx: &mut Context<picker::Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let hit = &self.matches[ix];
+
+        let highlighted_location = HighlightedMatch {
+            text: hit.string.clone(),
+            highlight_positions: hit.positions.clone(),
+            char_count: hit.string.chars().count(),
+            color: Color::Default,
+        };
+
+        let icon = Icon::new(IconName::FileTree)
+            .color(Color::Muted)
+            .size(ui::IconSize::Small);
+
+        Some(
+            ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
+                .inset(true)
+                .start_slot::<Icon>(icon)
+                .spacing(ListItemSpacing::Sparse)
+                .toggle_state(selected)
+                .child(highlighted_location.render(window, cx)),
+        )
     }
 }

crates/debugger_ui/src/persistence.rs 🔗

@@ -1,5 +1,5 @@
 use collections::HashMap;
-use dap::Capabilities;
+use dap::{Capabilities, adapters::DebugAdapterName};
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{Axis, Context, Entity, EntityId, Focusable, Subscription, WeakEntity, Window};
 use project::Project;
@@ -90,7 +90,7 @@ pub(crate) struct SerializedPane {
 const DEBUGGER_PANEL_PREFIX: &str = "debugger_panel_";
 
 pub(crate) async fn serialize_pane_layout(
-    adapter_name: SharedString,
+    adapter_name: DebugAdapterName,
     pane_group: SerializedPaneLayout,
 ) -> anyhow::Result<()> {
     if let Ok(serialized_pane_group) = serde_json::to_string(&pane_group) {

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

@@ -15,7 +15,9 @@ use breakpoint_list::BreakpointList;
 use collections::{HashMap, IndexMap};
 use console::Console;
 use dap::{
-    Capabilities, RunInTerminalRequestArguments, Thread, client::SessionId,
+    Capabilities, RunInTerminalRequestArguments, Thread,
+    adapters::{DebugAdapterName, DebugTaskDefinition},
+    client::SessionId,
     debugger_settings::DebuggerSettings,
 };
 use futures::{SinkExt, channel::mpsc};
@@ -23,6 +25,7 @@ use gpui::{
     Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
     NoAction, Pixels, Point, Subscription, Task, WeakEntity,
 };
+use language::Buffer;
 use loaded_source_list::LoadedSourceList;
 use module_list::ModuleList;
 use project::{
@@ -34,6 +37,10 @@ use rpc::proto::ViewId;
 use serde_json::Value;
 use settings::Settings;
 use stack_frame_list::StackFrameList;
+use task::{
+    DebugScenario, LaunchRequest, TaskContext, substitute_variables_in_map,
+    substitute_variables_in_str,
+};
 use terminal_view::TerminalView;
 use ui::{
     ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
@@ -667,6 +674,143 @@ impl RunningState {
         self.panes.pane_at_pixel_position(position).is_some()
     }
 
+    pub(crate) fn resolve_scenario(
+        &self,
+        scenario: DebugScenario,
+        task_context: TaskContext,
+        buffer: Option<Entity<Buffer>>,
+        window: &Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<DebugTaskDefinition>> {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return Task::ready(Err(anyhow!("no workspace")));
+        };
+        let project = workspace.read(cx).project().clone();
+        let dap_store = project.read(cx).dap_store().downgrade();
+        let task_store = project.read(cx).task_store().downgrade();
+        let weak_project = project.downgrade();
+        let weak_workspace = workspace.downgrade();
+        cx.spawn_in(window, async move |this, cx| {
+            let DebugScenario {
+                adapter,
+                label,
+                build,
+                request,
+                initialize_args,
+                tcp_connection,
+                stop_on_entry,
+            } = scenario;
+            let request = if let Some(request) = 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 terminal = project
+                    .update_in(cx, |project, window, cx| {
+                        project.create_terminal(
+                            TerminalKind::Task(task.resolved.clone()),
+                            window.window_handle(),
+                            cx,
+                        )
+                    })?
+                    .await?;
+
+                let terminal_view = cx.new_window_entity(|window, cx| {
+                    TerminalView::new(
+                        terminal.clone(),
+                        weak_workspace,
+                        None,
+                        weak_project,
+                        false,
+                        window,
+                        cx,
+                    )
+                })?;
+
+                this.update_in(cx, |this, window, cx| {
+                    this.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
+                    this.debug_terminal.update(cx, |debug_terminal, cx| {
+                        debug_terminal.terminal = Some(terminal_view);
+                        cx.notify();
+                    });
+                })?;
+
+                let exit_status = terminal
+                    .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
+                    .await
+                    .ok_or_else(|| anyhow!("Failed to wait for completed task"))?;
+
+                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"));
+            };
+            let request = match request {
+                dap::DebugRequest::Launch(launch_request) => {
+                    let cwd = match launch_request.cwd.as_deref().and_then(|path| path.to_str()) {
+                        Some(cwd) => {
+                            let substituted_cwd = substitute_variables_in_str(&cwd, &task_context)
+                                .ok_or_else(|| anyhow!("Failed to substitute variables in cwd"))?;
+                            Some(PathBuf::from(substituted_cwd))
+                        }
+                        None => None,
+                    };
+
+                    let env = substitute_variables_in_map(
+                        &launch_request.env.into_iter().collect(),
+                        &task_context,
+                    )
+                    .ok_or_else(|| anyhow!("Failed to substitute variables in env"))?
+                    .into_iter()
+                    .collect();
+                    let new_launch_request = LaunchRequest {
+                        program: substitute_variables_in_str(
+                            &launch_request.program,
+                            &task_context,
+                        )
+                        .ok_or_else(|| anyhow!("Failed to substitute variables in program"))?,
+                        args: launch_request
+                            .args
+                            .into_iter()
+                            .map(|arg| substitute_variables_in_str(&arg, &task_context))
+                            .collect::<Option<Vec<_>>>()
+                            .ok_or_else(|| anyhow!("Failed to substitute variables in args"))?,
+                        cwd,
+                        env,
+                    };
+
+                    dap::DebugRequest::Launch(new_launch_request)
+                }
+                request @ dap::DebugRequest::Attach(_) => request,
+            };
+            Ok(DebugTaskDefinition {
+                label,
+                adapter: DebugAdapterName(adapter),
+                request,
+                initialize_args,
+                stop_on_entry,
+                tcp_connection,
+            })
+        })
+    }
+
     fn handle_run_in_terminal(
         &self,
         request: &RunInTerminalRequestArguments,
@@ -914,7 +1058,7 @@ impl RunningState {
 
                 let Some((adapter_name, pane_group)) = this
                     .update(cx, |this, cx| {
-                        let adapter_name = this.session.read(cx).adapter_name();
+                        let adapter_name = this.session.read(cx).adapter();
                         (
                             adapter_name,
                             persistence::build_serialized_pane_layout(&this.panes.root, cx),

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

@@ -444,11 +444,13 @@ async fn test_handle_start_debugging_request(
                 .read(cx)
                 .session(cx);
             let parent_session = active_session.read(cx).parent_session().unwrap();
+            let mut original_binary = parent_session.read(cx).binary().clone();
+            original_binary.request_args = StartDebuggingRequestArguments {
+                request: StartDebuggingRequestArgumentsRequest::Launch,
+                configuration: fake_config.clone(),
+            };
 
-            assert_eq!(
-                active_session.read(cx).definition(),
-                parent_session.read(cx).definition()
-            );
+            assert_eq!(active_session.read(cx).binary(), &original_binary);
         })
         .unwrap();
 

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

@@ -50,7 +50,7 @@ use std::{
     sync::{Arc, Once},
 };
 use task::{DebugScenario, SpawnInTerminal};
-use util::ResultExt as _;
+use util::{ResultExt as _, merge_json_value_into};
 use worktree::Worktree;
 
 #[derive(Debug)]
@@ -393,7 +393,8 @@ impl DapStore {
 
     pub fn new_session(
         &mut self,
-        template: DebugTaskDefinition,
+        label: SharedString,
+        adapter: DebugAdapterName,
         parent_session: Option<Entity<Session>>,
         cx: &mut Context<Self>,
     ) -> Entity<Session> {
@@ -409,7 +410,8 @@ impl DapStore {
             self.breakpoint_store.clone(),
             session_id,
             parent_session,
-            template.clone(),
+            label,
+            adapter,
             cx,
         );
 
@@ -435,6 +437,7 @@ impl DapStore {
     pub fn boot_session(
         &self,
         session: Entity<Session>,
+        definition: DebugTaskDefinition,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
         let Some(worktree) = self.worktree_store.read(cx).visible_worktrees(cx).next() else {
@@ -442,17 +445,20 @@ impl DapStore {
         };
 
         let dap_store = cx.weak_entity();
-        let definition = session.read(cx).definition();
 
         cx.spawn({
             let session = session.clone();
             async move |this, cx| {
-                let binary = this
+                let mut binary = this
                     .update(cx, |this, cx| {
                         this.get_debug_adapter_binary(definition.clone(), cx)
                     })?
                     .await?;
 
+                if let Some(args) = definition.initialize_args {
+                    merge_json_value_into(args, &mut binary.request_args.configuration);
+                }
+
                 session
                     .update(cx, |session, cx| {
                         session.boot(binary, worktree, dap_store, cx)

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, DebugTaskDefinition};
+use dap::adapters::{DebugAdapterBinary, DebugAdapterName};
 use dap::messages::Response;
 use dap::requests::{Request, RunInTerminal, StartDebugging};
 use dap::{
@@ -32,7 +32,7 @@ use gpui::{
     Task, WeakEntity,
 };
 
-use serde_json::{Value, json};
+use serde_json::Value;
 use smol::stream::StreamExt;
 use std::any::TypeId;
 use std::collections::BTreeMap;
@@ -45,7 +45,7 @@ use std::{
     sync::Arc,
 };
 use text::{PointUtf16, ToPointUtf16};
-use util::{ResultExt, merge_json_value_into};
+use util::ResultExt;
 use worktree::Worktree;
 
 #[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
@@ -307,18 +307,11 @@ impl LocalMode {
     fn initialize_sequence(
         &self,
         capabilities: &Capabilities,
-        definition: &DebugTaskDefinition,
         initialized_rx: oneshot::Receiver<()>,
         dap_store: WeakEntity<DapStore>,
-        breakpoint_store: Entity<BreakpointStore>,
         cx: &App,
     ) -> Task<Result<()>> {
-        let mut raw = self.binary.request_args.clone();
-
-        merge_json_value_into(
-            definition.initialize_args.clone().unwrap_or(json!({})),
-            &mut raw.configuration,
-        );
+        let raw = self.binary.request_args.clone();
 
         // Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
         let launch = match raw.request {
@@ -349,6 +342,8 @@ impl LocalMode {
         let worktree = self.worktree().clone();
         let configuration_sequence = cx.spawn({
             async move |cx| {
+                let breakpoint_store =
+                    dap_store.update(cx, |dap_store, _| dap_store.breakpoint_store().clone())?;
                 initialized_rx.await?;
                 let errors_by_path = cx
                     .update(|cx| this.send_source_breakpoints(false, &breakpoint_store, cx))?
@@ -505,9 +500,10 @@ pub struct OutputToken(pub usize);
 /// Represents a current state of a single debug adapter and provides ways to mutate it.
 pub struct Session {
     pub mode: Mode,
-    definition: DebugTaskDefinition,
-    pub(super) capabilities: Capabilities,
     id: SessionId,
+    label: SharedString,
+    adapter: DebugAdapterName,
+    pub(super) capabilities: Capabilities,
     child_session_ids: HashSet<SessionId>,
     parent_session: Option<Entity<Session>>,
     modules: Vec<dap::Module>,
@@ -636,7 +632,8 @@ impl Session {
         breakpoint_store: Entity<BreakpointStore>,
         session_id: SessionId,
         parent_session: Option<Entity<Session>>,
-        template: DebugTaskDefinition,
+        label: SharedString,
+        adapter: DebugAdapterName,
         cx: &mut App,
     ) -> Entity<Self> {
         cx.new::<Self>(|cx| {
@@ -685,7 +682,8 @@ impl Session {
                 ignore_breakpoints: false,
                 breakpoint_store,
                 exception_breakpoints: Default::default(),
-                definition: template,
+                label,
+                adapter,
             };
 
             this
@@ -805,16 +803,12 @@ impl Session {
         &local_mode.binary
     }
 
-    pub fn adapter_name(&self) -> SharedString {
-        self.definition.adapter.clone()
+    pub fn adapter(&self) -> DebugAdapterName {
+        self.adapter.clone()
     }
 
     pub fn label(&self) -> SharedString {
-        self.definition.label.clone()
-    }
-
-    pub fn definition(&self) -> DebugTaskDefinition {
-        self.definition.clone()
+        self.label.clone()
     }
 
     pub fn is_terminated(&self) -> bool {
@@ -943,7 +937,7 @@ impl Session {
     }
 
     pub(super) fn request_initialize(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
-        let adapter_id = String::from(self.definition.adapter.clone());
+        let adapter_id = self.adapter().to_string();
         let request = Initialize { adapter_id };
         match &self.mode {
             Mode::Running(local_mode) => {
@@ -983,14 +977,9 @@ impl Session {
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
         match &self.mode {
-            Mode::Running(local_mode) => local_mode.initialize_sequence(
-                &self.capabilities,
-                &self.definition,
-                initialize_rx,
-                dap_store,
-                self.breakpoint_store.clone(),
-                cx,
-            ),
+            Mode::Running(local_mode) => {
+                local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx)
+            }
             Mode::Building => Task::ready(Err(anyhow!("cannot initialize, still building"))),
         }
     }

crates/project/src/project.rs 🔗

@@ -66,7 +66,7 @@ use image_store::{ImageItemEvent, ImageStoreEvent};
 use ::git::{blame::Blame, status::FileStatus};
 use gpui::{
     AnyEntity, App, AppContext, AsyncApp, BorrowAppContext, Context, Entity, EventEmitter, Hsla,
-    SharedString, Task, WeakEntity, Window, prelude::FluentBuilder,
+    SharedString, Task, WeakEntity, Window,
 };
 use itertools::Itertools;
 use language::{
@@ -3565,10 +3565,9 @@ impl Project {
     ) -> Task<anyhow::Result<Vec<InlayHint>>> {
         let snapshot = buffer_handle.read(cx).snapshot();
 
-        let Some(inline_value_provider) = session
-            .read(cx)
-            .adapter_name()
-            .map(|adapter_name| DapRegistry::global(cx).adapter(&adapter_name))
+        let adapter = session.read(cx).adapter();
+        let Some(inline_value_provider) = DapRegistry::global(cx)
+            .adapter(&adapter)
             .and_then(|adapter| adapter.inline_value_provider())
         else {
             return Task::ready(Err(anyhow::anyhow!("Inline value provider not found")));

crates/task/src/lib.rs 🔗

@@ -20,7 +20,8 @@ pub use debug_format::{
 };
 pub use task_template::{
     DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates,
-    substitute_all_template_variables_in_str,
+    substitute_all_template_variables_in_str, substitute_variables_in_map,
+    substitute_variables_in_str,
 };
 pub use vscode_debug_format::VsCodeDebugTaskFile;
 pub use vscode_format::VsCodeTaskFile;

crates/task/src/task_template.rs 🔗

@@ -293,6 +293,28 @@ fn to_hex_hash(object: impl Serialize) -> anyhow::Result<String> {
     Ok(hex::encode(hasher.finalize()))
 }
 
+pub fn substitute_variables_in_str(template_str: &str, context: &TaskContext) -> Option<String> {
+    let mut variable_names = HashMap::default();
+    let mut substituted_variables = HashSet::default();
+    let task_variables = context
+        .task_variables
+        .0
+        .iter()
+        .map(|(key, value)| {
+            let key_string = key.to_string();
+            if !variable_names.contains_key(&key_string) {
+                variable_names.insert(key_string.clone(), key.clone());
+            }
+            (key_string, value.as_str())
+        })
+        .collect::<HashMap<_, _>>();
+    substitute_all_template_variables_in_str(
+        template_str,
+        &task_variables,
+        &variable_names,
+        &mut substituted_variables,
+    )
+}
 pub fn substitute_all_template_variables_in_str<A: AsRef<str>>(
     template_str: &str,
     task_variables: &HashMap<String, A>,
@@ -349,6 +371,31 @@ fn substitute_all_template_variables_in_vec(
     Some(expanded)
 }
 
+pub fn substitute_variables_in_map(
+    keys_and_values: &HashMap<String, String>,
+    context: &TaskContext,
+) -> Option<HashMap<String, String>> {
+    let mut variable_names = HashMap::default();
+    let mut substituted_variables = HashSet::default();
+    let task_variables = context
+        .task_variables
+        .0
+        .iter()
+        .map(|(key, value)| {
+            let key_string = key.to_string();
+            if !variable_names.contains_key(&key_string) {
+                variable_names.insert(key_string.clone(), key.clone());
+            }
+            (key_string, value.as_str())
+        })
+        .collect::<HashMap<_, _>>();
+    substitute_all_template_variables_in_map(
+        keys_and_values,
+        &task_variables,
+        &variable_names,
+        &mut substituted_variables,
+    )
+}
 fn substitute_all_template_variables_in_map(
     keys_and_values: &HashMap<String, String>,
     task_variables: &HashMap<String, &str>,