debugger: Update New Session Modal (#30018)

Anthony Eid created

This PR simplifies the new session modal by flattening its three modes
and updating the UI to be less noisy. The new UI also defaults to the
Debug Scenario Picker, and allows users to save debug scenarios created
in the UI to the active worktree's .zed/debug.json file.


Release Notes:

- N/A

Change summary

Cargo.lock                                   |   1 
crates/debugger_ui/Cargo.toml                |   1 
crates/debugger_ui/src/attach_modal.rs       |   8 
crates/debugger_ui/src/debugger_panel.rs     |  71 +
crates/debugger_ui/src/debugger_ui.rs        |  31 
crates/debugger_ui/src/new_session_modal.rs  | 803 +++++++++------------
crates/debugger_ui/src/session/running.rs    |   2 
crates/debugger_ui/src/tests/attach_modal.rs |   2 
crates/languages/src/json.rs                 |   2 
crates/paths/src/paths.rs                    |   6 
crates/task/src/debug_format.rs              |   8 
crates/zed/src/zed.rs                        |   2 
12 files changed, 447 insertions(+), 490 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4199,6 +4199,7 @@ dependencies = [
  "log",
  "menu",
  "parking_lot",
+ "paths",
  "picker",
  "pretty_assertions",
  "project",

crates/debugger_ui/Cargo.toml 🔗

@@ -43,6 +43,7 @@ language.workspace = true
 log.workspace = true
 menu.workspace = true
 parking_lot.workspace = true
+paths.workspace = true
 picker.workspace = true
 pretty_assertions.workspace = true
 project.workspace = true

crates/debugger_ui/src/attach_modal.rs 🔗

@@ -32,12 +32,12 @@ pub(crate) struct AttachModalDelegate {
 
 impl AttachModalDelegate {
     fn new(
-        workspace: Entity<Workspace>,
+        workspace: WeakEntity<Workspace>,
         definition: DebugTaskDefinition,
         candidates: Arc<[Candidate]>,
     ) -> Self {
         Self {
-            workspace: workspace.downgrade(),
+            workspace,
             definition,
             candidates,
             selected_index: 0,
@@ -55,7 +55,7 @@ pub struct AttachModal {
 impl AttachModal {
     pub fn new(
         definition: DebugTaskDefinition,
-        workspace: Entity<Workspace>,
+        workspace: WeakEntity<Workspace>,
         modal: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -82,7 +82,7 @@ impl AttachModal {
     }
 
     pub(super) fn with_processes(
-        workspace: Entity<Workspace>,
+        workspace: WeakEntity<Workspace>,
         definition: DebugTaskDefinition,
         processes: Arc<[Candidate]>,
         modal: bool,

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -5,15 +5,15 @@ use crate::{
     FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart, StepBack,
     StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints, persistence,
 };
-use anyhow::Result;
+use anyhow::{Result, anyhow};
 use command_palette_hooks::CommandPaletteFilter;
+use dap::StartDebuggingRequestArguments;
 use dap::adapters::DebugAdapterName;
 use dap::debugger_settings::DebugPanelDockPosition;
 use dap::{
     ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
     client::SessionId, debugger_settings::DebuggerSettings,
 };
-use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
 use gpui::{
     Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
     FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
@@ -54,12 +54,11 @@ pub enum DebugPanelEvent {
 }
 
 actions!(debug_panel, [ToggleFocus]);
+
 pub struct DebugPanel {
     size: Pixels,
     sessions: Vec<Entity<DebugSession>>,
     active_session: Option<Entity<DebugSession>>,
-    /// This represents the last debug definition that was created in the new session modal
-    pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
     project: Entity<Project>,
     workspace: WeakEntity<Workspace>,
     focus_handle: FocusHandle,
@@ -80,7 +79,6 @@ impl DebugPanel {
                 size: px(300.),
                 sessions: vec![],
                 active_session: None,
-                past_debug_definition: None,
                 focus_handle: cx.focus_handle(),
                 project,
                 workspace: workspace.weak_handle(),
@@ -992,6 +990,69 @@ impl DebugPanel {
         self.active_session = Some(session_item);
         cx.notify();
     }
+
+    pub(crate) fn save_scenario(
+        &self,
+        scenario: &DebugScenario,
+        worktree_id: WorktreeId,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Task<Result<()>> {
+        self.workspace
+            .update(cx, |workspace, cx| {
+                let Some(mut path) = workspace.absolute_path_of_worktree(worktree_id, cx) else {
+                    return Task::ready(Err(anyhow!("Couldn't get worktree path")));
+                };
+
+                let serialized_scenario = serde_json::to_value(scenario);
+
+                path.push(paths::local_debug_file_relative_path());
+
+                cx.spawn_in(window, async move |workspace, cx| {
+                    let serialized_scenario = serialized_scenario?;
+                    let path = path.as_path();
+                    let fs =
+                        workspace.update(cx, |workspace, _| workspace.app_state().fs.clone())?;
+
+                    if !fs.is_file(path).await {
+                        let content =
+                            serde_json::to_string_pretty(&serde_json::Value::Array(vec![
+                                serialized_scenario,
+                            ]))?;
+
+                        fs.create_file(path, Default::default()).await?;
+                        fs.save(path, &content.into(), Default::default()).await?;
+                    } else {
+                        let content = fs.load(path).await?;
+                        let mut values = serde_json::from_str::<Vec<serde_json::Value>>(&content)?;
+                        values.push(serialized_scenario);
+                        fs.save(
+                            path,
+                            &serde_json::to_string_pretty(&values).map(Into::into)?,
+                            Default::default(),
+                        )
+                        .await?;
+                    }
+
+                    workspace.update_in(cx, |workspace, window, cx| {
+                        if let Some(project_path) = workspace
+                            .project()
+                            .read(cx)
+                            .project_path_for_absolute_path(&path, cx)
+                        {
+                            workspace.open_path(project_path, None, true, window, cx)
+                        } else {
+                            Task::ready(Err(anyhow!(
+                                "Couldn't get project path for .zed/debug.json in active worktree"
+                            )))
+                        }
+                    })?.await?;
+
+                    anyhow::Ok(())
+                })
+            })
+            .unwrap_or_else(|err| Task::ready(Err(err)))
+    }
 }
 
 impl EventEmitter<PanelEvent> for DebugPanel {}

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -147,36 +147,7 @@ pub fn init(cx: &mut App) {
                     },
                 )
                 .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
-                    if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
-                        let weak_panel = debug_panel.downgrade();
-                        let weak_workspace = cx.weak_entity();
-                        let task_store = workspace.project().read(cx).task_store().clone();
-
-                        cx.spawn_in(window, async move |this, cx| {
-                            let task_contexts = this
-                                .update_in(cx, |workspace, window, cx| {
-                                    tasks_ui::task_contexts(workspace, window, cx)
-                                })?
-                                .await;
-
-                            this.update_in(cx, |workspace, window, cx| {
-                                workspace.toggle_modal(window, cx, |window, cx| {
-                                    NewSessionModal::new(
-                                        debug_panel.read(cx).past_debug_definition.clone(),
-                                        weak_panel,
-                                        weak_workspace,
-                                        Some(task_store),
-                                        task_contexts,
-                                        window,
-                                        cx,
-                                    )
-                                });
-                            })?;
-
-                            anyhow::Ok(())
-                        })
-                        .detach()
-                    }
+                    NewSessionModal::show(workspace, window, cx);
                 });
         })
     })

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -29,7 +29,7 @@ use ui::{
     relative, rems, v_flex,
 };
 use util::ResultExt;
-use workspace::{ModalView, Workspace};
+use workspace::{ModalView, Workspace, pane};
 
 use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
 
@@ -37,11 +37,12 @@ pub(super) struct NewSessionModal {
     workspace: WeakEntity<Workspace>,
     debug_panel: WeakEntity<DebugPanel>,
     mode: NewSessionMode,
-    stop_on_entry: ToggleState,
-    initialize_args: Option<serde_json::Value>,
+    launch_picker: Entity<Picker<DebugScenarioDelegate>>,
+    attach_mode: Entity<AttachMode>,
+    custom_mode: Entity<CustomMode>,
     debugger: Option<DebugAdapterName>,
-    last_selected_profile_name: Option<SharedString>,
     task_contexts: Arc<TaskContexts>,
+    _subscriptions: [Subscription; 2],
 }
 
 fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
@@ -63,67 +64,126 @@ fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
 }
 
 impl NewSessionModal {
-    pub(super) fn new(
-        past_debug_definition: Option<DebugTaskDefinition>,
-        debug_panel: WeakEntity<DebugPanel>,
-        workspace: WeakEntity<Workspace>,
-        task_store: Option<Entity<TaskStore>>,
-        task_contexts: TaskContexts,
+    pub(super) fn show(
+        workspace: &mut Workspace,
         window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let debugger = past_debug_definition
-            .as_ref()
-            .map(|def| def.adapter.clone());
+        cx: &mut Context<Workspace>,
+    ) {
+        let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
+            return;
+        };
+        let task_store = workspace.project().read(cx).task_store().clone();
 
-        let stop_on_entry = past_debug_definition
-            .as_ref()
-            .and_then(|def| def.stop_on_entry);
+        cx.spawn_in(window, async move |workspace, cx| {
+            let task_contexts = Arc::from(
+                workspace
+                    .update_in(cx, |workspace, window, cx| {
+                        tasks_ui::task_contexts(workspace, window, cx)
+                    })?
+                    .await,
+            );
+
+            workspace.update_in(cx, |workspace, window, cx| {
+                let workspace_handle = workspace.weak_handle();
+                workspace.toggle_modal(window, cx, |window, cx| {
+                    let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
+
+                    let launch_picker = cx.new(|cx| {
+                        Picker::uniform_list(
+                            DebugScenarioDelegate::new(
+                                debug_panel.downgrade(),
+                                workspace_handle.clone(),
+                                task_store,
+                                task_contexts.clone(),
+                            ),
+                            window,
+                            cx,
+                        )
+                        .modal(false)
+                    });
 
-        let launch_config = match past_debug_definition.map(|def| def.request) {
-            Some(DebugRequest::Launch(launch_config)) => Some(launch_config),
-            _ => None,
-        };
+                    let _subscriptions = [
+                        cx.subscribe(&launch_picker, |_, _, _, cx| {
+                            cx.emit(DismissEvent);
+                        }),
+                        cx.subscribe(
+                            &attach_mode.read(cx).attach_picker.clone(),
+                            |_, _, _, cx| {
+                                cx.emit(DismissEvent);
+                            },
+                        ),
+                    ];
+
+                    let custom_mode = CustomMode::new(None, window, cx);
+
+                    Self {
+                        launch_picker,
+                        attach_mode,
+                        custom_mode,
+                        debugger: None,
+                        mode: NewSessionMode::Launch,
+                        debug_panel: debug_panel.downgrade(),
+                        workspace: workspace_handle,
+                        task_contexts,
+                        _subscriptions,
+                    }
+                });
+            })?;
 
-        if let Some(task_store) = task_store {
-            cx.defer_in(window, |this, window, cx| {
-                this.mode = NewSessionMode::scenario(
-                    this.debug_panel.clone(),
-                    this.workspace.clone(),
-                    task_store,
-                    window,
-                    cx,
-                );
-            });
-        };
+            anyhow::Ok(())
+        })
+        .detach();
+    }
 
-        Self {
-            workspace: workspace.clone(),
-            debugger,
-            debug_panel,
-            mode: NewSessionMode::launch(launch_config, window, cx),
-            stop_on_entry: stop_on_entry
-                .map(Into::into)
-                .unwrap_or(ToggleState::Unselected),
-            last_selected_profile_name: None,
-            initialize_args: None,
-            task_contexts: Arc::new(task_contexts),
+    fn render_mode(&self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+        let dap_menu = self.adapter_drop_down_menu(window, cx);
+        match self.mode {
+            NewSessionMode::Attach => self.attach_mode.update(cx, |this, cx| {
+                this.clone().render(window, cx).into_any_element()
+            }),
+            NewSessionMode::Custom => self.custom_mode.update(cx, |this, cx| {
+                this.clone().render(dap_menu, window, cx).into_any_element()
+            }),
+            NewSessionMode::Launch => v_flex()
+                .w(rems(34.))
+                .child(self.launch_picker.clone())
+                .into_any_element(),
+        }
+    }
+
+    fn mode_focus_handle(&self, cx: &App) -> FocusHandle {
+        match self.mode {
+            NewSessionMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
+            NewSessionMode::Custom => self.custom_mode.read(cx).program.focus_handle(cx),
+            NewSessionMode::Launch => self.launch_picker.focus_handle(cx),
         }
     }
 
-    fn debug_config(&self, cx: &App, debugger: &str) -> Option<DebugScenario> {
-        let request = self.mode.debug_task(cx)?;
+    fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
+        let request = match self.mode {
+            NewSessionMode::Custom => Some(DebugRequest::Launch(
+                self.custom_mode.read(cx).debug_request(cx),
+            )),
+            NewSessionMode::Attach => Some(DebugRequest::Attach(
+                self.attach_mode.read(cx).debug_request(),
+            )),
+            _ => None,
+        }?;
         let label = suggested_label(&request, debugger);
+
+        let stop_on_entry = if let NewSessionMode::Custom = &self.mode {
+            Some(self.custom_mode.read(cx).stop_on_entry.selected())
+        } else {
+            None
+        };
+
         Some(DebugScenario {
             adapter: debugger.to_owned().into(),
             label,
             request: Some(request),
-            initialize_args: self.initialize_args.clone(),
+            initialize_args: None,
             tcp_connection: None,
-            stop_on_entry: match self.stop_on_entry {
-                ToggleState::Selected => Some(true),
-                _ => None,
-            },
+            stop_on_entry,
             build: None,
         })
     }
@@ -135,14 +195,14 @@ impl NewSessionModal {
             return;
         };
 
-        if let NewSessionMode::Scenario(picker) = &self.mode {
-            picker.update(cx, |picker, cx| {
+        if let NewSessionMode::Launch = &self.mode {
+            self.launch_picker.update(cx, |picker, cx| {
                 picker.delegate.confirm(false, window, cx);
             });
             return;
         }
 
-        let Some(config) = self.debug_config(cx, debugger) else {
+        let Some(config) = self.debug_scenario(debugger, cx) else {
             log::error!("debug config not found in mode: {}", self.mode);
             return;
         };
@@ -189,7 +249,7 @@ impl NewSessionModal {
         &self,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Option<ui::DropdownMenu> {
+    ) -> ui::DropdownMenu {
         let workspace = self.workspace.clone();
         let weak = cx.weak_entity();
         let label = self
@@ -207,6 +267,7 @@ impl NewSessionModal {
                     .and_then(|location| location.buffer.read(cx).language())
             })
             .cloned();
+
         DropdownMenu::new(
             "dap-adapter-picker",
             label,
@@ -217,8 +278,8 @@ impl NewSessionModal {
                         weak.update(cx, |this, cx| {
                             this.debugger = Some(name.clone());
                             cx.notify();
-                            if let NewSessionMode::Attach(attach) = &this.mode {
-                                Self::update_attach_picker(&attach, &name, window, cx);
+                            if let NewSessionMode::Attach = &this.mode {
+                                Self::update_attach_picker(&this.attach_mode, &name, window, cx);
                             }
                         })
                         .ok();
@@ -227,7 +288,6 @@ impl NewSessionModal {
 
                 let mut available_adapters = workspace
                     .update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
-                    .ok()
                     .unwrap_or_default();
                 if let Some(language) = active_buffer_language {
                     available_adapters.sort_by_key(|adapter| {
@@ -245,195 +305,24 @@ impl NewSessionModal {
                 menu
             }),
         )
-        .into()
-    }
-
-    fn debug_config_drop_down_menu(
-        &self,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> ui::DropdownMenu {
-        let workspace = self.workspace.clone();
-        let weak = cx.weak_entity();
-        let last_profile = self.last_selected_profile_name.clone();
-        let worktree = workspace
-            .update(cx, |this, cx| {
-                this.project().read(cx).visible_worktrees(cx).next()
-            })
-            .unwrap_or_default();
-        DropdownMenu::new(
-            "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: 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(DebugAdapterName(task.adapter.clone()));
-                            this.initialize_args = task.initialize_args.clone();
-                            match &task.request {
-                                Some(DebugRequest::Launch(launch_config)) => {
-                                    this.mode = NewSessionMode::launch(
-                                        Some(launch_config.clone()),
-                                        window,
-                                        cx,
-                                    );
-                                }
-                                Some(DebugRequest::Attach(_)) => {
-                                    let Some(workspace) = this.workspace.upgrade() else {
-                                        return;
-                                    };
-                                    this.mode = NewSessionMode::attach(
-                                        this.debugger.clone(),
-                                        workspace,
-                                        window,
-                                        cx,
-                                    );
-                                    this.mode.focus_handle(cx).focus(window);
-                                    if let Some((debugger, attach)) =
-                                        this.debugger.as_ref().zip(this.mode.as_attach())
-                                    {
-                                        Self::update_attach_picker(&attach, &debugger, window, cx);
-                                    }
-                                }
-                                _ => log::warn!("Selected debug scenario without either attach or launch request specified"),
-                            }
-                            cx.notify();
-                        })
-                        .ok();
-                    }
-                };
-
-                let available_tasks: Vec<DebugScenario> = workspace
-                    .update(cx, |this, cx| {
-                        this.project()
-                            .read(cx)
-                            .task_store()
-                            .read(cx)
-                            .task_inventory()
-                            .iter()
-                            .flat_map(|task_inventory| {
-                                task_inventory.read(cx).list_debug_scenarios(
-                                    worktree
-                                        .as_ref()
-                                        .map(|worktree| worktree.read(cx).id())
-                                        .iter()
-                                        .copied(),
-                                )
-                            })
-                            .map(|(_source_kind, scenario)| scenario)
-                            .collect()
-                    })
-                    .ok()
-                    .unwrap_or_default();
-
-                for debug_definition in available_tasks {
-                    menu = menu.entry(
-                        debug_definition.label.clone(),
-                        None,
-                        setter_for_name(debug_definition),
-                    );
-                }
-                menu
-            }),
-        )
     }
 }
 
 static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
-static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
 
 #[derive(Clone)]
 enum NewSessionMode {
-    Launch(Entity<LaunchMode>),
-    Scenario(Entity<Picker<DebugScenarioDelegate>>),
-    Attach(Entity<AttachMode>),
-}
-
-impl NewSessionMode {
-    fn debug_task(&self, cx: &App) -> Option<DebugRequest> {
-        match self {
-            NewSessionMode::Launch(entity) => Some(entity.read(cx).debug_task(cx).into()),
-            NewSessionMode::Attach(entity) => Some(entity.read(cx).debug_task().into()),
-            NewSessionMode::Scenario(_) => None,
-        }
-    }
-    fn as_attach(&self) -> Option<&Entity<AttachMode>> {
-        if let NewSessionMode::Attach(entity) = self {
-            Some(entity)
-        } else {
-            None
-        }
-    }
-
-    fn scenario(
-        debug_panel: WeakEntity<DebugPanel>,
-        workspace: WeakEntity<Workspace>,
-        task_store: Entity<TaskStore>,
-        window: &mut Window,
-        cx: &mut Context<NewSessionModal>,
-    ) -> NewSessionMode {
-        let picker = cx.new(|cx| {
-            Picker::uniform_list(
-                DebugScenarioDelegate::new(debug_panel, workspace, task_store),
-                window,
-                cx,
-            )
-            .modal(false)
-        });
-
-        cx.subscribe(&picker, |_, _, _, cx| {
-            cx.emit(DismissEvent);
-        })
-        .detach();
-
-        picker.focus_handle(cx).focus(window);
-        NewSessionMode::Scenario(picker)
-    }
-
-    fn attach(
-        debugger: Option<DebugAdapterName>,
-        workspace: Entity<Workspace>,
-        window: &mut Window,
-        cx: &mut Context<NewSessionModal>,
-    ) -> Self {
-        Self::Attach(AttachMode::new(debugger, workspace, window, cx))
-    }
-
-    fn launch(
-        past_launch_config: Option<LaunchRequest>,
-        window: &mut Window,
-        cx: &mut Context<NewSessionModal>,
-    ) -> Self {
-        Self::Launch(LaunchMode::new(past_launch_config, window, cx))
-    }
-
-    fn has_match(&self, cx: &App) -> bool {
-        match self {
-            NewSessionMode::Scenario(picker) => picker.read(cx).delegate.match_count() > 0,
-            NewSessionMode::Attach(picker) => {
-                picker
-                    .read(cx)
-                    .attach_picker
-                    .read(cx)
-                    .picker
-                    .read(cx)
-                    .delegate
-                    .match_count()
-                    > 0
-            }
-            _ => false,
-        }
-    }
+    Custom,
+    Attach,
+    Launch,
 }
 
 impl std::fmt::Display for NewSessionMode {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         let mode = match self {
-            NewSessionMode::Launch(_) => "launch".to_owned(),
-            NewSessionMode::Attach(_) => "attach".to_owned(),
-            NewSessionMode::Scenario(_) => "scenario picker".to_owned(),
+            NewSessionMode::Launch => "Launch".to_owned(),
+            NewSessionMode::Attach => "Attach".to_owned(),
+            NewSessionMode::Custom => "Custom".to_owned(),
         };
 
         write!(f, "{}", mode)
@@ -442,28 +331,7 @@ impl std::fmt::Display for NewSessionMode {
 
 impl Focusable for NewSessionMode {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
-        match &self {
-            NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
-            NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx),
-            NewSessionMode::Scenario(entity) => entity.read(cx).focus_handle(cx),
-        }
-    }
-}
-
-impl RenderOnce for NewSessionMode {
-    fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
-        match self {
-            NewSessionMode::Launch(entity) => entity.update(cx, |this, cx| {
-                this.clone().render(window, cx).into_any_element()
-            }),
-            NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
-                this.clone().render(window, cx).into_any_element()
-            }),
-            NewSessionMode::Scenario(entity) => v_flex()
-                .w(rems(34.))
-                .child(entity.clone())
-                .into_any_element(),
-        }
+        cx.focus_handle()
     }
 }
 
@@ -514,11 +382,36 @@ impl Render for NewSessionModal {
         v_flex()
             .size_full()
             .w(rems(34.))
+            .key_context("Pane")
             .elevation_3(cx)
             .bg(cx.theme().colors().elevated_surface_background)
             .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
                 cx.emit(DismissEvent);
             }))
+            .on_action(
+                cx.listener(|this, _: &pane::ActivatePreviousItem, window, cx| {
+                    this.mode = match this.mode {
+                        NewSessionMode::Attach => NewSessionMode::Launch,
+                        NewSessionMode::Launch => NewSessionMode::Attach,
+                        _ => {
+                            return;
+                        }
+                    };
+
+                    this.mode_focus_handle(cx).focus(window);
+                }),
+            )
+            .on_action(cx.listener(|this, _: &pane::ActivateNextItem, window, cx| {
+                this.mode = match this.mode {
+                    NewSessionMode::Attach => NewSessionMode::Launch,
+                    NewSessionMode::Launch => NewSessionMode::Attach,
+                    _ => {
+                        return;
+                    }
+                };
+
+                this.mode_focus_handle(cx).focus(window);
+            }))
             .child(
                 h_flex()
                     .w_full()
@@ -529,84 +422,44 @@ impl Render for NewSessionModal {
                             .justify_start()
                             .w_full()
                             .child(
-                                ToggleButton::new("debugger-session-ui-picker-button", "Scenarios")
+                                ToggleButton::new("debugger-session-ui-picker-button", "Launch")
                                     .size(ButtonSize::Default)
                                     .style(ui::ButtonStyle::Subtle)
-                                    .toggle_state(matches!(self.mode, NewSessionMode::Scenario(_)))
+                                    .toggle_state(matches!(self.mode, NewSessionMode::Launch))
                                     .on_click(cx.listener(|this, _, window, cx| {
-                                        let Some(task_store) = this
-                                            .workspace
-                                            .update(cx, |workspace, cx| {
-                                                workspace.project().read(cx).task_store().clone()
-                                            })
-                                            .ok()
-                                        else {
-                                            return;
-                                        };
-
-                                        this.mode = NewSessionMode::scenario(
-                                            this.debug_panel.clone(),
-                                            this.workspace.clone(),
-                                            task_store,
-                                            window,
-                                            cx,
-                                        );
-
+                                        this.mode = NewSessionMode::Launch;
+                                        this.mode_focus_handle(cx).focus(window);
                                         cx.notify();
                                     }))
                                     .first(),
                             )
                             .child(
-                                ToggleButton::new(
-                                    "debugger-session-ui-launch-button",
-                                    "New Session",
-                                )
-                                .size(ButtonSize::Default)
-                                .style(ui::ButtonStyle::Subtle)
-                                .toggle_state(matches!(self.mode, NewSessionMode::Launch(_)))
-                                .on_click(cx.listener(|this, _, window, cx| {
-                                    this.mode = NewSessionMode::launch(None, window, cx);
-                                    this.mode.focus_handle(cx).focus(window);
-                                    cx.notify();
-                                }))
-                                .middle(),
-                            )
-                            .child(
-                                ToggleButton::new(
-                                    "debugger-session-ui-attach-button",
-                                    "Attach to Process",
-                                )
-                                .size(ButtonSize::Default)
-                                .toggle_state(matches!(self.mode, NewSessionMode::Attach(_)))
-                                .style(ui::ButtonStyle::Subtle)
-                                .on_click(cx.listener(|this, _, window, cx| {
-                                    let Some(workspace) = this.workspace.upgrade() else {
-                                        return;
-                                    };
-                                    this.mode = NewSessionMode::attach(
-                                        this.debugger.clone(),
-                                        workspace,
-                                        window,
-                                        cx,
-                                    );
-                                    this.mode.focus_handle(cx).focus(window);
-                                    if let Some((debugger, attach)) =
-                                        this.debugger.as_ref().zip(this.mode.as_attach())
-                                    {
-                                        Self::update_attach_picker(&attach, &debugger, window, cx);
-                                    }
-
-                                    cx.notify();
-                                }))
-                                .last(),
+                                ToggleButton::new("debugger-session-ui-attach-button", "Attach")
+                                    .size(ButtonSize::Default)
+                                    .toggle_state(matches!(self.mode, NewSessionMode::Attach))
+                                    .style(ui::ButtonStyle::Subtle)
+                                    .on_click(cx.listener(|this, _, window, cx| {
+                                        this.mode = NewSessionMode::Attach;
+
+                                        if let Some(debugger) = this.debugger.as_ref() {
+                                            Self::update_attach_picker(
+                                                &this.attach_mode,
+                                                &debugger,
+                                                window,
+                                                cx,
+                                            );
+                                        }
+                                        this.mode_focus_handle(cx).focus(window);
+                                        cx.notify();
+                                    }))
+                                    .last(),
                             ),
                     )
                     .justify_between()
-                    .children(self.adapter_drop_down_menu(window, cx))
                     .border_color(cx.theme().colors().border_variant)
                     .border_b_1(),
             )
-            .child(v_flex().child(self.mode.clone().render(window, cx)))
+            .child(v_flex().child(self.render_mode(window, cx)))
             .child(
                 h_flex()
                     .justify_between()
@@ -615,53 +468,91 @@ impl Render for NewSessionModal {
                     .border_color(cx.theme().colors().border_variant)
                     .border_t_1()
                     .w_full()
-                    .child(
-                        matches!(self.mode, NewSessionMode::Scenario(_))
-                            .not()
-                            .then(|| {
-                                self.debug_config_drop_down_menu(window, cx)
-                                    .into_any_element()
-                            })
-                            .unwrap_or_else(|| v_flex().w_full().into_any_element()),
-                    )
-                    .child(
-                        h_flex()
-                            .justify_end()
-                            .when(matches!(self.mode, NewSessionMode::Launch(_)), |this| {
-                                let weak = cx.weak_entity();
-                                this.child(
-                                    CheckboxWithLabel::new(
-                                        "debugger-stop-on-entry",
-                                        Label::new("Stop on Entry").size(ui::LabelSize::Small),
-                                        self.stop_on_entry,
-                                        move |state, _, cx| {
-                                            weak.update(cx, |this, _| {
-                                                this.stop_on_entry = *state;
-                                            })
-                                            .ok();
-                                        },
-                                    )
-                                    .checkbox_position(ui::IconPosition::End),
-                                )
-                            })
-                            .child(
-                                Button::new("debugger-spawn", "Start")
-                                    .on_click(cx.listener(|this, _, window, cx| match &this.mode {
-                                        NewSessionMode::Scenario(picker) => {
-                                            picker.update(cx, |picker, cx| {
-                                                picker.delegate.confirm(true, window, cx)
-                                            })
-                                        }
-                                        _ => this.start_new_session(window, cx),
-                                    }))
-                                    .disabled(match self.mode {
-                                        NewSessionMode::Scenario(_) => !self.mode.has_match(cx),
-                                        NewSessionMode::Attach(_) => {
-                                            self.debugger.is_none() || !self.mode.has_match(cx)
+                    .child(match self.mode {
+                        NewSessionMode::Attach => {
+                            div().child(self.adapter_drop_down_menu(window, cx))
+                        }
+                        NewSessionMode::Launch => div().child(
+                            Button::new("new-session-modal-custom", "Custom").on_click({
+                                let this = cx.weak_entity();
+                                move |_, window, cx| {
+                                    this.update(cx, |this, cx| {
+                                        this.mode = NewSessionMode::Custom;
+                                        this.mode_focus_handle(cx).focus(window);
+                                    })
+                                    .ok();
+                                }
+                            }),
+                        ),
+                        NewSessionMode::Custom => div().child(
+                            Button::new("new-session-modal-back", "Save to .zed/debug.json...")
+                                .on_click(cx.listener(|this, _, window, cx| {
+                                    let Some(save_scenario_task) = this
+                                        .debugger
+                                        .as_ref()
+                                        .and_then(|debugger| this.debug_scenario(&debugger, cx))
+                                        .zip(this.task_contexts.worktree())
+                                        .and_then(|(scenario, worktree_id)| {
+                                            this.debug_panel
+                                                .update(cx, |panel, cx| {
+                                                    panel.save_scenario(
+                                                        &scenario,
+                                                        worktree_id,
+                                                        window,
+                                                        cx,
+                                                    )
+                                                })
+                                                .ok()
+                                        })
+                                    else {
+                                        return;
+                                    };
+
+                                    cx.spawn(async move |this, cx| {
+                                        if save_scenario_task.await.is_ok() {
+                                            this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
                                         }
-                                        NewSessionMode::Launch(_) => self.debugger.is_none(),
-                                    }),
-                            ),
+                                    })
+                                    .detach();
+                                }))
+                                .disabled(
+                                    self.debugger.is_none()
+                                        || self.custom_mode.read(cx).program.read(cx).is_empty(cx),
+                                ),
+                        ),
+                    })
+                    .child(
+                        Button::new("debugger-spawn", "Start")
+                            .on_click(cx.listener(|this, _, window, cx| match &this.mode {
+                                NewSessionMode::Launch => {
+                                    this.launch_picker.update(cx, |picker, cx| {
+                                        picker.delegate.confirm(true, window, cx)
+                                    })
+                                }
+                                _ => this.start_new_session(window, cx),
+                            }))
+                            .disabled(match self.mode {
+                                NewSessionMode::Launch => {
+                                    !self.launch_picker.read(cx).delegate.matches.is_empty()
+                                }
+                                NewSessionMode::Attach => {
+                                    self.debugger.is_none()
+                                        || self
+                                            .attach_mode
+                                            .read(cx)
+                                            .attach_picker
+                                            .read(cx)
+                                            .picker
+                                            .read(cx)
+                                            .delegate
+                                            .match_count()
+                                            == 0
+                                }
+                                NewSessionMode::Custom => {
+                                    self.debugger.is_none()
+                                        || self.custom_mode.read(cx).program.read(cx).is_empty(cx)
+                                }
+                            }),
                     ),
             )
     }
@@ -670,38 +561,12 @@ impl Render for NewSessionModal {
 impl EventEmitter<DismissEvent> for NewSessionModal {}
 impl Focusable for NewSessionModal {
     fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
-        self.mode.focus_handle(cx)
+        self.mode_focus_handle(cx)
     }
 }
 
 impl ModalView for NewSessionModal {}
 
-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()
@@ -711,15 +576,14 @@ impl RenderOnce for AttachMode {
     }
 }
 
-use std::rc::Rc;
-
 #[derive(Clone)]
-pub(super) struct LaunchMode {
+pub(super) struct CustomMode {
     program: Entity<Editor>,
     cwd: Entity<Editor>,
+    stop_on_entry: ToggleState,
 }
 
-impl LaunchMode {
+impl CustomMode {
     pub(super) fn new(
         past_launch_config: Option<LaunchRequest>,
         window: &mut Window,
@@ -744,10 +608,14 @@ impl LaunchMode {
                 this.set_text(past_cwd.to_string_lossy(), window, cx);
             };
         });
-        cx.new(|_| Self { program, cwd })
+        cx.new(|_| Self {
+            program,
+            cwd,
+            stop_on_entry: ToggleState::Unselected,
+        })
     }
 
-    pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
+    pub(super) fn debug_request(&self, cx: &App) -> task::LaunchRequest {
         let path = self.cwd.read(cx).text(cx);
         task::LaunchRequest {
             program: self.program.read(cx).text(cx),
@@ -756,19 +624,66 @@ impl LaunchMode {
             env: Default::default(),
         }
     }
+
+    fn render(
+        &mut self,
+        adapter_menu: DropdownMenu,
+        window: &mut Window,
+        cx: &mut ui::Context<Self>,
+    ) -> 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(
+                h_flex()
+                    .child(
+                        Label::new("Debugger")
+                            .size(ui::LabelSize::Small)
+                            .color(Color::Muted),
+                    )
+                    .gap(ui::DynamicSpacing::Base08.rems(cx))
+                    .child(adapter_menu),
+            )
+            .child(
+                CheckboxWithLabel::new(
+                    "debugger-stop-on-entry",
+                    Label::new("Stop on Entry").size(ui::LabelSize::Small),
+                    self.stop_on_entry,
+                    {
+                        let this = cx.weak_entity();
+                        move |state, _, cx| {
+                            this.update(cx, |this, _| {
+                                this.stop_on_entry = *state;
+                            })
+                            .ok();
+                        }
+                    },
+                )
+                .checkbox_position(ui::IconPosition::End),
+            )
+    }
 }
 
 #[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<DebugAdapterName>,
-        workspace: Entity<Workspace>,
+        workspace: WeakEntity<Workspace>,
         window: &mut Window,
         cx: &mut Context<NewSessionModal>,
     ) -> Entity<Self> {
@@ -787,17 +702,12 @@ impl AttachMode {
             modal
         });
 
-        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 {
+    pub(super) fn debug_request(&self) -> task::AttachRequest {
         task::AttachRequest { process_id: None }
     }
 }
@@ -810,6 +720,7 @@ pub(super) struct DebugScenarioDelegate {
     prompt: String,
     debug_panel: WeakEntity<DebugPanel>,
     workspace: WeakEntity<Workspace>,
+    task_contexts: Arc<TaskContexts>,
 }
 
 impl DebugScenarioDelegate {
@@ -817,6 +728,7 @@ impl DebugScenarioDelegate {
         debug_panel: WeakEntity<DebugPanel>,
         workspace: WeakEntity<Workspace>,
         task_store: Entity<TaskStore>,
+        task_contexts: Arc<TaskContexts>,
     ) -> Self {
         Self {
             task_store,
@@ -826,6 +738,7 @@ impl DebugScenarioDelegate {
             prompt: String::new(),
             debug_panel,
             workspace,
+            task_contexts,
         }
     }
 }
@@ -860,45 +773,55 @@ impl PickerDelegate for DebugScenarioDelegate {
         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();
-
-                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());
+        let candidates = self.candidates.clone();
+        let workspace = self.workspace.clone();
+        let task_store = self.task_store.clone();
 
-                scenarios
+        cx.spawn_in(window, async move |picker, cx| {
+            let candidates: Vec<_> = match &candidates {
+                Some(candidates) => candidates
                     .into_iter()
                     .enumerate()
                     .map(|(index, (_, candidate))| {
                         StringMatchCandidate::new(index, candidate.label.as_ref())
                     })
-                    .collect()
-            }
-        };
+                    .collect(),
+                None => {
+                    let worktree_ids: Vec<_> = workspace
+                        .update(cx, |this, cx| {
+                            this.visible_worktrees(cx)
+                                .map(|tree| tree.read(cx).id())
+                                .collect()
+                        })
+                        .ok()
+                        .unwrap_or_default();
+
+                    let scenarios: Vec<_> = task_store
+                        .update(cx, |task_store, cx| {
+                            task_store.task_inventory().map(|item| {
+                                item.read(cx).list_debug_scenarios(worktree_ids.into_iter())
+                            })
+                        })
+                        .ok()
+                        .flatten()
+                        .unwrap_or_default();
+
+                    picker
+                        .update(cx, |picker, _| {
+                            picker.delegate.candidates = Some(scenarios.clone());
+                        })
+                        .ok();
+
+                    scenarios
+                        .into_iter()
+                        .enumerate()
+                        .map(|(index, (_, candidate))| {
+                            StringMatchCandidate::new(index, candidate.label.as_ref())
+                        })
+                        .collect()
+                }
+            };
 
-        cx.spawn_in(window, async move |picker, cx| {
             let matches = fuzzy::match_strings(
                 &candidates,
                 &query,

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

@@ -864,7 +864,7 @@ impl RunningState {
 
                     dap::DebugRequest::Launch(new_launch_request)
                 }
-                request @ dap::DebugRequest::Attach(_) => request,
+                request @ dap::DebugRequest::Attach(_) => request, // todo(debugger): We should check that process_id is valid and if not show the modal
             };
             Ok(DebugTaskDefinition {
                 label,

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

@@ -103,7 +103,7 @@ async fn test_show_attach_modal_and_select_process(
         });
     let attach_modal = workspace
         .update(cx, |workspace, window, cx| {
-            let workspace_handle = cx.entity();
+            let workspace_handle = cx.weak_entity();
             workspace.toggle_modal(window, cx, |window, cx| {
                 AttachModal::with_processes(
                     workspace_handle,

crates/languages/src/json.rs 🔗

@@ -141,7 +141,7 @@ impl JsonLspAdapter {
                     },
                     {
                         "fileMatch": [
-                            schema_file_match(paths::debug_tasks_file()),
+                            schema_file_match(paths::debug_scenarios_file()),
                             paths::local_debug_file_relative_path()
                         ],
                         "schema": debug_schema,

crates/paths/src/paths.rs 🔗

@@ -216,9 +216,9 @@ pub fn tasks_file() -> &'static PathBuf {
 }
 
 /// Returns the path to the `debug.json` file.
-pub fn debug_tasks_file() -> &'static PathBuf {
-    static DEBUG_TASKS_FILE: OnceLock<PathBuf> = OnceLock::new();
-    DEBUG_TASKS_FILE.get_or_init(|| config_dir().join("debug.json"))
+pub fn debug_scenarios_file() -> &'static PathBuf {
+    static DEBUG_SCENARIOS_FILE: OnceLock<PathBuf> = OnceLock::new();
+    DEBUG_SCENARIOS_FILE.get_or_init(|| config_dir().join("debug.json"))
 }
 
 /// Returns the path to the extensions directory.

crates/task/src/debug_format.rs 🔗

@@ -193,22 +193,22 @@ pub struct DebugScenario {
     /// Name of the debug task
     pub label: SharedString,
     /// A task to run prior to spawning the debuggee.
-    #[serde(default)]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub build: Option<BuildTaskDefinition>,
     #[serde(flatten)]
     pub request: Option<DebugRequest>,
     /// Additional initialization arguments to be sent on DAP initialization
-    #[serde(default)]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     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)]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub tcp_connection: Option<TcpArgumentsTemplate>,
     /// Whether to tell the debug adapter to stop on entry
-    #[serde(default)]
+    #[serde(default, skip_serializing_if = "Option::is_none")]
     pub stop_on_entry: Option<bool>,
 }
 

crates/zed/src/zed.rs 🔗

@@ -701,7 +701,7 @@ fn register_actions(
         })
         .register_action(move |_: &mut Workspace, _: &OpenDebugTasks, window, cx| {
             open_settings_file(
-                paths::debug_tasks_file(),
+                paths::debug_scenarios_file(),
                 || settings::initial_debug_tasks_content().as_ref().into(),
                 window,
                 cx,