debugger: Fix debug scenario's defined in debug.json not using passed in build task (#29973)

Anthony Eid created

There were two bugs that caused user-defined debug scenarios from being
able to run a build task.

1. DebugRequest would be deserialized to `Attach` even when `process_id`
wasn't defined in a user's configuration file. This has been fixed by
adding our own deserializer that defaults to None if there are no fields
present instead of `Attach`, and I added tests to prevent regressions.
2. Debug scenario resolve phase never got the active buffer when
spawning a debug session from the new session modal. This has been
worked around by passing in the worktree_id of a debug scenario in the
scenario picker and the active worktree_id otherwise.

Release Notes:

- N/A

Change summary

crates/debugger_ui/src/attach_modal.rs      |  2 
crates/debugger_ui/src/debugger_panel.rs    |  6 +
crates/debugger_ui/src/new_session_modal.rs | 16 +++
crates/debugger_ui/src/session/running.rs   |  5 
crates/project/src/task_inventory.rs        |  5 
crates/task/src/debug_format.rs             | 75 ++++++++++++++++++++++
6 files changed, 97 insertions(+), 12 deletions(-)

Detailed changes

crates/debugger_ui/src/attach_modal.rs 🔗

@@ -237,7 +237,7 @@ impl PickerDelegate for AttachModalDelegate {
             .flatten();
         if let Some(panel) = panel {
             panel.update(cx, |panel, cx| {
-                panel.start_session(scenario, Default::default(), None, window, cx);
+                panel.start_session(scenario, Default::default(), None, None, window, cx);
             });
         }
 

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -22,8 +22,8 @@ use gpui::{
 };
 
 use language::Buffer;
-use project::Fs;
 use project::debugger::session::{Session, SessionStateEvent};
+use project::{Fs, WorktreeId};
 use project::{Project, debugger::session::ThreadStatus};
 use rpc::proto::{self};
 use settings::Settings;
@@ -208,6 +208,7 @@ impl DebugPanel {
         scenario: DebugScenario,
         task_context: TaskContext,
         active_buffer: Option<Entity<Buffer>>,
+        worktree_id: Option<WorktreeId>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
@@ -233,6 +234,7 @@ impl DebugPanel {
                                 scenario,
                                 task_context,
                                 active_buffer,
+                                worktree_id,
                                 window,
                                 cx,
                             )
@@ -1283,7 +1285,7 @@ impl workspace::DebuggerProvider for DebuggerProvider {
     ) {
         self.0.update(cx, |_, cx| {
             cx.defer_in(window, |this, window, cx| {
-                this.start_session(definition, context, buffer, window, cx);
+                this.start_session(definition, context, buffer, None, window, cx);
             })
         })
     }

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -150,6 +150,7 @@ impl NewSessionModal {
             let task_contexts = workspace
                 .update_in(cx, |this, window, cx| task_contexts(this, window, cx))?
                 .await;
+            let worktree_id = task_contexts.worktree();
             let task_context = task_contexts
                 .active_item_context
                 .map(|(_, _, context)| context)
@@ -159,8 +160,9 @@ impl NewSessionModal {
                         .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)
+                debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
             })?;
             this.update(cx, |_, cx| {
                 cx.emit(DismissEvent);
@@ -937,19 +939,27 @@ impl PickerDelegate for DebugScenarioDelegate {
                     .await
                     .task_context_for_worktree_id(worktree_id)
                     .cloned()
+                    .map(|context| (context, Some(worktree_id)))
             })
         } else {
             gpui::Task::ready(None)
         };
 
         cx.spawn_in(window, async move |this, cx| {
-            let task_context = task_context.await.unwrap_or_default();
+            let (task_context, worktree_id) = task_context.await.unwrap_or_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);
+                        panel.start_session(
+                            debug_scenario,
+                            task_context,
+                            None,
+                            worktree_id,
+                            window,
+                            cx,
+                        );
                     })
                     .ok();
 

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

@@ -29,7 +29,7 @@ use language::Buffer;
 use loaded_source_list::LoadedSourceList;
 use module_list::ModuleList;
 use project::{
-    Project,
+    Project, WorktreeId,
     debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
     terminals::TerminalKind,
 };
@@ -684,6 +684,7 @@ impl RunningState {
         scenario: DebugScenario,
         task_context: TaskContext,
         buffer: Option<Entity<Buffer>>,
+        worktree_id: Option<WorktreeId>,
         window: &Window,
         cx: &mut Context<Self>,
     ) -> Task<Result<DebugTaskDefinition>> {
@@ -712,7 +713,7 @@ impl RunningState {
                     this.task_inventory().and_then(|inventory| {
                         inventory
                             .read(cx)
-                            .task_template_by_label(buffer, &build, cx)
+                            .task_template_by_label(buffer, worktree_id, &build, cx)
                     })
                 })?
                 else {

crates/project/src/task_inventory.rs 🔗

@@ -229,10 +229,11 @@ impl Inventory {
     pub fn task_template_by_label(
         &self,
         buffer: Option<Entity<Buffer>>,
+        worktree_id: Option<WorktreeId>,
         label: &str,
         cx: &App,
     ) -> Option<TaskTemplate> {
-        let (worktree_id, file, language) = buffer
+        let (buffer_worktree_id, file, language) = buffer
             .map(|buffer| {
                 let buffer = buffer.read(cx);
                 let file = buffer.file().cloned();
@@ -244,7 +245,7 @@ impl Inventory {
             })
             .unwrap_or((None, None, None));
 
-        self.list_tasks(file, language, worktree_id, cx)
+        self.list_tasks(file, language, worktree_id.or(buffer_worktree_id), cx)
             .iter()
             .find(|(_, template)| template.label == label)
             .map(|val| val.1.clone())

crates/task/src/debug_format.rs 🔗

@@ -47,12 +47,35 @@ impl TcpArgumentsTemplate {
 }
 
 /// Represents the attach request information of the debug adapter
-#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
+#[derive(Default, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 pub struct AttachRequest {
     /// The processId to attach to, if left empty we will show a process picker
     pub process_id: Option<u32>,
 }
 
+impl<'de> Deserialize<'de> for AttachRequest {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        #[derive(Deserialize)]
+        struct Helper {
+            process_id: Option<u32>,
+        }
+
+        let helper = Helper::deserialize(deserializer)?;
+
+        // Skip creating an AttachRequest if process_id is None
+        if helper.process_id.is_none() {
+            return Err(serde::de::Error::custom("process_id is required"));
+        }
+
+        Ok(AttachRequest {
+            process_id: helper.process_id,
+        })
+    }
+}
+
 /// Represents the launch request information of the debug adapter
 #[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)]
 pub struct LaunchRequest {
@@ -204,7 +227,7 @@ impl DebugTaskFile {
 
 #[cfg(test)]
 mod tests {
-    use crate::{DebugRequest, LaunchRequest};
+    use crate::{DebugRequest, DebugScenario, LaunchRequest};
 
     #[test]
     fn test_can_deserialize_non_attach_task() {
@@ -218,4 +241,52 @@ mod tests {
             })
         );
     }
+
+    #[test]
+    fn test_empty_scenario_has_none_request() {
+        let json = r#"{
+            "label": "Build & debug rust",
+            "build": "rust",
+            "adapter": "CodeLLDB"
+        }"#;
+
+        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
+        assert_eq!(deserialized.request, None);
+    }
+
+    #[test]
+    fn test_launch_scenario_deserialization() {
+        let json = r#"{
+            "label": "Launch program",
+            "adapter": "CodeLLDB",
+            "program": "target/debug/myapp",
+            "args": ["--test"]
+        }"#;
+
+        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
+        match deserialized.request {
+            Some(DebugRequest::Launch(launch)) => {
+                assert_eq!(launch.program, "target/debug/myapp");
+                assert_eq!(launch.args, vec!["--test"]);
+            }
+            _ => panic!("Expected Launch request"),
+        }
+    }
+
+    #[test]
+    fn test_attach_scenario_deserialization() {
+        let json = r#"{
+            "label": "Attach to process",
+            "adapter": "CodeLLDB",
+            "process_id": 1234
+        }"#;
+
+        let deserialized: DebugScenario = serde_json::from_str(json).unwrap();
+        match deserialized.request {
+            Some(DebugRequest::Attach(attach)) => {
+                assert_eq!(attach.process_id, Some(1234));
+            }
+            _ => panic!("Expected Attach request"),
+        }
+    }
 }