debugger: Start on tabless design (#27837)

Piotr Osiewicz , Anthony Eid , and Anthony created

![image](https://github.com/user-attachments/assets/1cd54b70-5457-4c64-95bd-45a7055ea165)

Release Notes:

- N/A

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
Co-authored-by: Anthony <anthony@zed.dev>

Change summary

.zed/debug.json                                            |   4 
assets/icons/arrow_down_right.svg                          |   1 
assets/icons/arrow_up_right.svg                            |   4 
assets/icons/bug_off.svg                                   |   1 
assets/icons/circle_off.svg                                |   1 
assets/icons/power.svg                                     |   1 
crates/dap_adapters/src/lldb.rs                            |   2 
crates/dap_adapters/src/python.rs                          |  37 
crates/debugger_ui/src/attach_modal.rs                     |   5 
crates/debugger_ui/src/debugger_panel.rs                   | 429 ++++-
crates/debugger_ui/src/debugger_ui.rs                      |  49 
crates/debugger_ui/src/new_session_modal.rs                | 633 ++++++++
crates/debugger_ui/src/session.rs                          | 202 --
crates/debugger_ui/src/session/failed.rs                   |  30 
crates/debugger_ui/src/session/inert.rs                    | 337 ----
crates/debugger_ui/src/session/running.rs                  | 316 ---
crates/debugger_ui/src/session/running/console.rs          |   9 
crates/debugger_ui/src/session/running/module_list.rs      |   6 
crates/debugger_ui/src/session/running/stack_frame_list.rs |  12 
crates/debugger_ui/src/session/running/variable_list.rs    |  16 
crates/debugger_ui/src/session/starting.rs                 |  80 -
crates/debugger_ui/src/tests/attach_modal.rs               |   1 
crates/debugger_ui/src/tests/debugger_panel.rs             |  26 
crates/icons/src/icons.rs                                  |   4 
crates/project/src/debugger/dap_store.rs                   |   5 
crates/project/src/debugger/session.rs                     |   6 
crates/project/src/task_inventory.rs                       |  16 
crates/task/src/debug_format.rs                            |  64 
crates/tasks_ui/src/modal.rs                               |   1 
crates/tasks_ui/src/tasks_ui.rs                            |   3 
crates/ui/src/components/button/toggle_button.rs           |  12 
crates/ui/src/components/toggle.rs                         |  46 
crates/workspace/src/workspace.rs                          |  18 
33 files changed, 1,286 insertions(+), 1,091 deletions(-)

Detailed changes

.zed/debug.json 🔗

@@ -1,14 +1,14 @@
 [
   {
     "label": "Debug Zed with LLDB",
-    "adapter": "lldb",
+    "adapter": "LLDB",
     "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
     "request": "launch",
     "cwd": "$ZED_WORKTREE_ROOT"
   },
   {
     "label": "Debug Zed with GDB",
-    "adapter": "gdb",
+    "adapter": "GDB",
     "program": "$ZED_WORKTREE_ROOT/target/debug/zed",
     "request": "launch",
     "cwd": "$ZED_WORKTREE_ROOT",

assets/icons/arrow_down_right.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down-right-icon lucide-arrow-down-right"><path d="m7 7 10 10"/><path d="M17 7v10H7"/></svg>

assets/icons/arrow_up_right.svg 🔗

@@ -1,3 +1 @@
-<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M6 2H6.5C6.5 1.86739 6.44732 1.74021 6.35355 1.64645C6.25979 1.55268 6.13261 1.5 6 1.5V2ZM2 1.5C1.72386 1.5 1.5 1.72386 1.5 2C1.5 2.27614 1.72386 2.5 2 2.5L2 1.5ZM5.5 6C5.5 6.27614 5.72386 6.5 6 6.5C6.27614 6.5 6.5 6.27614 6.5 6H5.5ZM1.64645 5.64645C1.45118 5.84171 1.45118 6.15829 1.64645 6.35355C1.84171 6.54882 2.15829 6.54882 2.35355 6.35355L1.64645 5.64645ZM6 1.5H2L2 2.5H6V1.5ZM5.5 2V6H6.5V2H5.5ZM5.64645 1.64645L1.64645 5.64645L2.35355 6.35355L6.35355 2.35355L5.64645 1.64645Z" fill="white"/>
-</svg>
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-up-right-icon lucide-arrow-up-right"><path d="M7 7h10v10"/><path d="M7 17 17 7"/></svg>

assets/icons/bug_off.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug-off-icon lucide-bug-off"><path d="M15 7.13V6a3 3 0 0 0-5.14-2.1L8 2"/><path d="M14.12 3.88 16 2"/><path d="M22 13h-4v-2a4 4 0 0 0-4-4h-1.3"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="m2 2 20 20"/><path d="M7.7 7.7A4 4 0 0 0 6 11v3a6 6 0 0 0 11.13 3.13"/><path d="M12 20v-8"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/></svg>

assets/icons/circle_off.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-off-icon lucide-circle-off"><path d="m2 2 20 20"/><path d="M8.35 2.69A10 10 0 0 1 21.3 15.65"/><path d="M19.08 19.08A10 10 0 1 1 4.92 4.92"/></svg>

assets/icons/power.svg 🔗

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-power-icon lucide-power"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>

crates/dap_adapters/src/lldb.rs 🔗

@@ -78,10 +78,10 @@ impl DebugAdapter for LldbDebugAdapter {
         match &config.request {
             DebugRequestType::Attach(attach) => {
                 map.insert("pid".into(), attach.process_id.into());
-                map.insert("stopOnEntry".into(), config.stop_on_entry.into());
             }
             DebugRequestType::Launch(launch) => {
                 map.insert("program".into(), launch.program.clone().into());
+                map.insert("stopOnEntry".into(), config.stop_on_entry.into());
                 map.insert("args".into(), launch.args.clone().into());
                 map.insert(
                     "cwd".into(),

crates/dap_adapters/src/python.rs 🔗

@@ -126,24 +126,31 @@ impl DebugAdapter for PythonDebugAdapter {
     }
 
     fn request_args(&self, config: &DebugTaskDefinition) -> Value {
+        let mut args = json!({
+            "request": match config.request {
+                DebugRequestType::Launch(_) => "launch",
+                DebugRequestType::Attach(_) => "attach",
+            },
+            "subProcess": true,
+            "redirectOutput": true,
+        });
+        let map = args.as_object_mut().unwrap();
         match &config.request {
-            DebugRequestType::Launch(launch_config) => {
-                json!({
-                    "program": launch_config.program,
-                    "args": launch_config.args,
-                    "subProcess": true,
-                    "cwd": launch_config.cwd,
-                    "redirectOutput": true,
-                    "StopOnEntry": config.stop_on_entry,
-                })
+            DebugRequestType::Attach(attach) => {
+                map.insert("processId".into(), attach.process_id.into());
             }
-            dap::DebugRequestType::Attach(attach_config) => {
-                json!({
-                    "subProcess": true,
-                    "redirectOutput": true,
-                    "processId": attach_config.process_id
-                })
+            DebugRequestType::Launch(launch) => {
+                map.insert("program".into(), launch.program.clone().into());
+                map.insert("args".into(), launch.args.clone().into());
+
+                if let Some(stop_on_entry) = config.stop_on_entry {
+                    map.insert("stopOnEntry".into(), stop_on_entry.into());
+                }
+                if let Some(cwd) = launch.cwd.as_ref() {
+                    map.insert("cwd".into(), cwd.to_string_lossy().into_owned().into());
+                }
             }
         }
+        args
     }
 }

crates/debugger_ui/src/attach_modal.rs 🔗

@@ -54,6 +54,7 @@ impl AttachModal {
     pub fn new(
         project: Entity<project::Project>,
         debug_config: task::DebugTaskDefinition,
+        modal: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -74,13 +75,14 @@ impl AttachModal {
             })
             .collect();
         processes.sort_by_key(|k| k.name.clone());
-        Self::with_processes(project, debug_config, processes, window, cx)
+        Self::with_processes(project, debug_config, processes, modal, window, cx)
     }
 
     pub(super) fn with_processes(
         project: Entity<project::Project>,
         debug_config: task::DebugTaskDefinition,
         processes: Vec<Candidate>,
+        modal: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -103,6 +105,7 @@ impl AttachModal {
                 window,
                 cx,
             )
+            .modal(modal)
         });
         Self {
             _subscription: cx.subscribe(&picker, |_, _, _, cx| {

crates/debugger_ui/src/debugger_panel.rs 🔗

@@ -1,4 +1,8 @@
-use crate::session::DebugSession;
+use crate::{
+    ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
+    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
+};
+use crate::{new_session_modal::NewSessionModal, session::DebugSession};
 use anyhow::{Result, anyhow};
 use collections::HashMap;
 use command_palette_hooks::CommandPaletteFilter;
@@ -13,7 +17,10 @@ use gpui::{
 };
 use project::{
     Project,
-    debugger::dap_store::{self, DapStore},
+    debugger::{
+        dap_store::{self, DapStore},
+        session::ThreadStatus,
+    },
     terminals::TerminalKind,
 };
 use rpc::proto::{self};
@@ -21,11 +28,9 @@ use settings::Settings;
 use std::{any::TypeId, path::PathBuf};
 use task::DebugTaskDefinition;
 use terminal_view::terminal_panel::TerminalPanel;
-use ui::prelude::*;
-use util::ResultExt;
+use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 use workspace::{
-    ClearAllBreakpoints, Continue, Disconnect, Pane, Pause, Restart, StepBack, StepInto, StepOut,
-    StepOver, Stop, ToggleIgnoreBreakpoints, Workspace,
+    Pane, Workspace,
     dock::{DockPosition, Panel, PanelEvent},
     pane,
 };
@@ -51,10 +56,11 @@ actions!(debug_panel, [ToggleFocus]);
 pub struct DebugPanel {
     size: Pixels,
     pane: Entity<Pane>,
+    /// This represents the last debug definition that was created in the new session modal
+    pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
     project: WeakEntity<Project>,
     workspace: WeakEntity<Workspace>,
     _subscriptions: Vec<Subscription>,
-    pub(crate) last_inert_config: Option<DebugTaskDefinition>,
 }
 
 impl DebugPanel {
@@ -66,8 +72,6 @@ impl DebugPanel {
         cx.new(|cx| {
             let project = workspace.project().clone();
             let dap_store = project.read(cx).dap_store();
-            let weak_workspace = workspace.weak_handle();
-            let debug_panel = cx.weak_entity();
             let pane = cx.new(|cx| {
                 let mut pane = Pane::new(
                     workspace.weak_handle(),
@@ -81,71 +85,9 @@ impl DebugPanel {
                 pane.set_can_split(None);
                 pane.set_can_navigate(true, cx);
                 pane.display_nav_history_buttons(None);
-                pane.set_should_display_tab_bar(|_window, _cx| true);
+                pane.set_should_display_tab_bar(|_window, _cx| false);
                 pane.set_close_pane_if_empty(true, cx);
-                pane.set_render_tab_bar_buttons(cx, {
-                    let project = project.clone();
-                    let weak_workspace = weak_workspace.clone();
-                    let debug_panel = debug_panel.clone();
-                    move |_, _, cx| {
-                        let project = project.clone();
-                        let weak_workspace = weak_workspace.clone();
-                        (
-                            None,
-                            Some(
-                                h_flex()
-                                    .child(
-                                        IconButton::new("new-debug-session", IconName::Plus)
-                                            .icon_size(IconSize::Small)
-                                            .on_click({
-                                                let debug_panel = debug_panel.clone();
-
-                                                cx.listener(move |pane, _, window, cx| {
-                                                    let config = debug_panel
-                                                        .read_with(cx, |this: &DebugPanel, _| {
-                                                            this.last_inert_config.clone()
-                                                        })
-                                                        .log_err()
-                                                        .flatten();
-
-                                                    pane.add_item(
-                                                        Box::new(DebugSession::inert(
-                                                            project.clone(),
-                                                            weak_workspace.clone(),
-                                                            debug_panel.clone(),
-                                                            config,
-                                                            window,
-                                                            cx,
-                                                        )),
-                                                        false,
-                                                        false,
-                                                        None,
-                                                        window,
-                                                        cx,
-                                                    );
-                                                })
-                                            }),
-                                    )
-                                    .into_any_element(),
-                            ),
-                        )
-                    }
-                });
-                pane.add_item(
-                    Box::new(DebugSession::inert(
-                        project.clone(),
-                        weak_workspace.clone(),
-                        debug_panel.clone(),
-                        None,
-                        window,
-                        cx,
-                    )),
-                    false,
-                    false,
-                    None,
-                    window,
-                    cx,
-                );
+
                 pane
             });
 
@@ -159,7 +101,7 @@ impl DebugPanel {
                 pane,
                 size: px(300.),
                 _subscriptions,
-                last_inert_config: None,
+                past_debug_definition: None,
                 project: project.downgrade(),
                 workspace: workspace.weak_handle(),
             };
@@ -295,7 +237,7 @@ impl DebugPanel {
         cx: &mut Context<Self>,
     ) {
         match event {
-            dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
+            dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
                 let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
                     return log::error!(
                         "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
@@ -470,6 +412,274 @@ impl DebugPanel {
             _ => {}
         }
     }
+
+    fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
+        let active_session = self
+            .pane
+            .read(cx)
+            .active_item()
+            .and_then(|item| item.downcast::<DebugSession>());
+        Some(
+            h_flex()
+                .border_b_1()
+                .border_color(cx.theme().colors().border)
+                .p_1()
+                .justify_between()
+                .w_full()
+                .child(
+                    h_flex().gap_2().w_full().when_some(
+                        active_session
+                            .as_ref()
+                            .and_then(|session| session.read(cx).mode().as_running()),
+                        |this, running_session| {
+                            let thread_status = running_session
+                                .read(cx)
+                                .thread_status(cx)
+                                .unwrap_or(project::debugger::session::ThreadStatus::Exited);
+                            let capabilities = running_session.read(cx).capabilities(cx);
+                            this.map(|this| {
+                                if thread_status == ThreadStatus::Running {
+                                    this.child(
+                                        IconButton::new("debug-pause", IconName::DebugPause)
+                                            .icon_size(IconSize::XSmall)
+                                            .shape(ui::IconButtonShape::Square)
+                                            .on_click(window.listener_for(
+                                                &running_session,
+                                                |this, _, _window, cx| {
+                                                    this.pause_thread(cx);
+                                                },
+                                            ))
+                                            .tooltip(move |window, cx| {
+                                                Tooltip::text("Pause program")(window, cx)
+                                            }),
+                                    )
+                                } else {
+                                    this.child(
+                                        IconButton::new("debug-continue", IconName::DebugContinue)
+                                            .icon_size(IconSize::XSmall)
+                                            .shape(ui::IconButtonShape::Square)
+                                            .on_click(window.listener_for(
+                                                &running_session,
+                                                |this, _, _window, cx| this.continue_thread(cx),
+                                            ))
+                                            .disabled(thread_status != ThreadStatus::Stopped)
+                                            .tooltip(move |window, cx| {
+                                                Tooltip::text("Continue program")(window, cx)
+                                            }),
+                                    )
+                                }
+                            })
+                            .child(
+                                IconButton::new("debug-step-over", IconName::ArrowRight)
+                                    .icon_size(IconSize::XSmall)
+                                    .shape(ui::IconButtonShape::Square)
+                                    .on_click(window.listener_for(
+                                        &running_session,
+                                        |this, _, _window, cx| {
+                                            this.step_over(cx);
+                                        },
+                                    ))
+                                    .disabled(thread_status != ThreadStatus::Stopped)
+                                    .tooltip(move |window, cx| {
+                                        Tooltip::text("Step over")(window, cx)
+                                    }),
+                            )
+                            .child(
+                                IconButton::new("debug-step-out", IconName::ArrowUpRight)
+                                    .icon_size(IconSize::XSmall)
+                                    .shape(ui::IconButtonShape::Square)
+                                    .on_click(window.listener_for(
+                                        &running_session,
+                                        |this, _, _window, cx| {
+                                            this.step_out(cx);
+                                        },
+                                    ))
+                                    .disabled(thread_status != ThreadStatus::Stopped)
+                                    .tooltip(move |window, cx| {
+                                        Tooltip::text("Step out")(window, cx)
+                                    }),
+                            )
+                            .child(
+                                IconButton::new("debug-step-into", IconName::ArrowDownRight)
+                                    .icon_size(IconSize::XSmall)
+                                    .shape(ui::IconButtonShape::Square)
+                                    .on_click(window.listener_for(
+                                        &running_session,
+                                        |this, _, _window, cx| {
+                                            this.step_in(cx);
+                                        },
+                                    ))
+                                    .disabled(thread_status != ThreadStatus::Stopped)
+                                    .tooltip(move |window, cx| {
+                                        Tooltip::text("Step in")(window, cx)
+                                    }),
+                            )
+                            .child(Divider::vertical())
+                            .child(
+                                IconButton::new(
+                                    "debug-enable-breakpoint",
+                                    IconName::DebugDisabledBreakpoint,
+                                )
+                                .icon_size(IconSize::XSmall)
+                                .shape(ui::IconButtonShape::Square)
+                                .disabled(thread_status != ThreadStatus::Stopped),
+                            )
+                            .child(
+                                IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
+                                    .icon_size(IconSize::XSmall)
+                                    .shape(ui::IconButtonShape::Square)
+                                    .disabled(thread_status != ThreadStatus::Stopped),
+                            )
+                            .child(
+                                IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
+                                    .icon_size(IconSize::XSmall)
+                                    .shape(ui::IconButtonShape::Square)
+                                    .disabled(
+                                        thread_status == ThreadStatus::Exited
+                                            || thread_status == ThreadStatus::Ended,
+                                    )
+                                    .on_click(window.listener_for(
+                                        &running_session,
+                                        |this, _, _window, cx| {
+                                            this.toggle_ignore_breakpoints(cx);
+                                        },
+                                    ))
+                                    .tooltip(move |window, cx| {
+                                        Tooltip::text("Disable all breakpoints")(window, cx)
+                                    }),
+                            )
+                            .child(Divider::vertical())
+                            .child(
+                                IconButton::new("debug-restart", IconName::DebugRestart)
+                                    .icon_size(IconSize::XSmall)
+                                    .on_click(window.listener_for(
+                                        &running_session,
+                                        |this, _, _window, cx| {
+                                            this.restart_session(cx);
+                                        },
+                                    ))
+                                    .disabled(
+                                        !capabilities.supports_restart_request.unwrap_or_default(),
+                                    )
+                                    .tooltip(move |window, cx| {
+                                        Tooltip::text("Restart")(window, cx)
+                                    }),
+                            )
+                            .child(
+                                IconButton::new("debug-stop", IconName::Power)
+                                    .icon_size(IconSize::XSmall)
+                                    .on_click(window.listener_for(
+                                        &running_session,
+                                        |this, _, _window, cx| {
+                                            this.stop_thread(cx);
+                                        },
+                                    ))
+                                    .disabled(
+                                        thread_status != ThreadStatus::Stopped
+                                            && thread_status != ThreadStatus::Running,
+                                    )
+                                    .tooltip({
+                                        let label = if capabilities
+                                            .supports_terminate_threads_request
+                                            .unwrap_or_default()
+                                        {
+                                            "Terminate Thread"
+                                        } else {
+                                            "Terminate all Threads"
+                                        };
+                                        move |window, cx| Tooltip::text(label)(window, cx)
+                                    }),
+                            )
+                        },
+                    ),
+                )
+                .child(
+                    h_flex()
+                        .gap_2()
+                        .when_some(
+                            active_session
+                                .as_ref()
+                                .and_then(|session| session.read(cx).mode().as_running())
+                                .cloned(),
+                            |this, session| {
+                                this.child(
+                                    session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
+                                )
+                                .child(Divider::vertical())
+                            },
+                        )
+                        .when_some(active_session.as_ref(), |this, session| {
+                            let pane = self.pane.downgrade();
+                            let label = session.read(cx).label(cx);
+                            this.child(DropdownMenu::new(
+                                "debugger-session-list",
+                                label,
+                                ContextMenu::build(window, cx, move |mut this, _, cx| {
+                                    let sessions = pane
+                                        .read_with(cx, |pane, _| {
+                                            pane.items().map(|item| item.boxed_clone()).collect()
+                                        })
+                                        .ok()
+                                        .unwrap_or_else(Vec::new);
+                                    for (index, item) in sessions.into_iter().enumerate() {
+                                        if let Some(session) = item.downcast::<DebugSession>() {
+                                            let pane = pane.clone();
+                                            this = this.entry(
+                                                session.read(cx).label(cx),
+                                                None,
+                                                move |window, cx| {
+                                                    pane.update(cx, |pane, cx| {
+                                                        pane.activate_item(
+                                                            index, true, true, window, cx,
+                                                        );
+                                                    })
+                                                    .ok();
+                                                },
+                                            );
+                                        }
+                                    }
+                                    this
+                                }),
+                            ))
+                            .child(Divider::vertical())
+                        })
+                        .child(
+                            IconButton::new("debug-new-session", IconName::Plus)
+                                .icon_size(IconSize::Small)
+                                .on_click({
+                                    let workspace = self.workspace.clone();
+                                    let weak_panel = cx.weak_entity();
+                                    let past_debug_definition = self.past_debug_definition.clone();
+                                    move |_, window, cx| {
+                                        let weak_panel = weak_panel.clone();
+                                        let past_debug_definition = past_debug_definition.clone();
+
+                                        let _ = workspace.update(cx, |this, cx| {
+                                            let workspace = cx.weak_entity();
+                                            this.toggle_modal(window, cx, |window, cx| {
+                                                NewSessionModal::new(
+                                                    past_debug_definition,
+                                                    weak_panel,
+                                                    workspace,
+                                                    window,
+                                                    cx,
+                                                )
+                                            });
+                                        });
+                                    }
+                                })
+                                .tooltip(|window, cx| {
+                                    Tooltip::for_action(
+                                        "New Debug Session",
+                                        &CreateDebuggingSession,
+                                        window,
+                                        cx,
+                                    )
+                                }),
+                        ),
+                ),
+        )
+    }
 }
 
 impl EventEmitter<PanelEvent> for DebugPanel {}
@@ -507,7 +717,7 @@ impl Panel for DebugPanel {
     ) {
     }
 
-    fn size(&self, _window: &Window, _cx: &App) -> Pixels {
+    fn size(&self, _window: &Window, _: &App) -> Pixels {
         self.size
     }
 
@@ -538,42 +748,49 @@ impl Panel for DebugPanel {
     fn activation_priority(&self) -> u32 {
         9
     }
-    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
-        if active && self.pane.read(cx).items_len() == 0 {
-            let Some(project) = self.project.clone().upgrade() else {
-                return;
-            };
-            let config = self.last_inert_config.clone();
-            let panel = cx.weak_entity();
-            // todo: We need to revisit it when we start adding stopped items to pane (as that'll cause us to add two items).
-            self.pane.update(cx, |this, cx| {
-                this.add_item(
-                    Box::new(DebugSession::inert(
-                        project,
-                        self.workspace.clone(),
-                        panel,
-                        config,
-                        window,
-                        cx,
-                    )),
-                    false,
-                    false,
-                    None,
-                    window,
-                    cx,
-                );
-            });
-        }
-    }
+    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
 }
 
 impl Render for DebugPanel {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let has_sessions = self.pane.read(cx).items_len() > 0;
         v_flex()
+            .size_full()
             .key_context("DebugPanel")
+            .child(h_flex().children(self.top_controls_strip(window, cx)))
             .track_focus(&self.focus_handle(cx))
-            .size_full()
-            .child(self.pane.clone())
+            .map(|this| {
+                if has_sessions {
+                    this.child(self.pane.clone())
+                } else {
+                    this.child(
+                        v_flex()
+                            .h_full()
+                            .gap_1()
+                            .items_center()
+                            .justify_center()
+                            .child(
+                                h_flex().child(
+                                    Label::new("No Debugging Sessions")
+                                        .size(LabelSize::Small)
+                                        .color(Color::Muted),
+                                ),
+                            )
+                            .child(
+                                h_flex().flex_shrink().child(
+                                    Button::new("spawn-new-session-empty-state", "New Session")
+                                        .size(ButtonSize::Large)
+                                        .on_click(|_, window, cx| {
+                                            window.dispatch_action(
+                                                CreateDebuggingSession.boxed_clone(),
+                                                cx,
+                                            );
+                                        }),
+                                ),
+                            ),
+                    )
+                }
+            })
             .into_any()
     }
 }

crates/debugger_ui/src/debugger_ui.rs 🔗

@@ -1,20 +1,38 @@
 use dap::debugger_settings::DebuggerSettings;
 use debugger_panel::{DebugPanel, ToggleFocus};
 use feature_flags::{Debugger, FeatureFlagViewExt};
-use gpui::App;
+use gpui::{App, actions};
+use new_session_modal::NewSessionModal;
 use session::DebugSession;
 use settings::Settings;
-use workspace::{
-    Pause, Restart, ShutdownDebugAdapters, StepBack, StepInto, StepOver, Stop,
-    ToggleIgnoreBreakpoints, Workspace,
-};
+use workspace::{ShutdownDebugAdapters, Workspace};
 
 pub mod attach_modal;
 pub mod debugger_panel;
-pub mod session;
+mod new_session_modal;
+pub(crate) mod session;
 
 #[cfg(test)]
-mod tests;
+pub mod tests;
+
+actions!(
+    debugger,
+    [
+        Start,
+        Continue,
+        Disconnect,
+        Pause,
+        Restart,
+        StepInto,
+        StepOver,
+        StepOut,
+        StepBack,
+        Stop,
+        ToggleIgnoreBreakpoints,
+        ClearAllBreakpoints,
+        CreateDebuggingSession,
+    ]
+);
 
 pub fn init(cx: &mut App) {
     DebuggerSettings::register(cx);
@@ -115,6 +133,23 @@ pub fn init(cx: &mut App) {
                             })
                         })
                     },
+                )
+                .register_action(
+                    |workspace: &mut Workspace, _: &CreateDebuggingSession, window, cx| {
+                        let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
+                        let weak_panel = debug_panel.downgrade();
+                        let weak_workspace = cx.weak_entity();
+
+                        workspace.toggle_modal(window, cx, |window, cx| {
+                            NewSessionModal::new(
+                                debug_panel.read(cx).past_debug_definition.clone(),
+                                weak_panel,
+                                weak_workspace,
+                                window,
+                                cx,
+                            )
+                        });
+                    },
                 );
         })
     })

crates/debugger_ui/src/new_session_modal.rs 🔗

@@ -0,0 +1,633 @@
+use std::{
+    borrow::Cow,
+    ops::Not,
+    path::{Path, PathBuf},
+};
+
+use anyhow::{Result, anyhow};
+use dap::DebugRequestType;
+use editor::{Editor, EditorElement, EditorStyle};
+use gpui::{
+    App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
+    WeakEntity,
+};
+use settings::Settings;
+use task::{DebugTaskDefinition, LaunchConfig};
+use theme::ThemeSettings;
+use ui::{
+    ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
+    ContextMenu, Disableable, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
+    LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
+    ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
+};
+use util::ResultExt;
+use workspace::{ModalView, Workspace};
+
+use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
+
+#[derive(Clone)]
+pub(super) struct NewSessionModal {
+    workspace: WeakEntity<Workspace>,
+    debug_panel: WeakEntity<DebugPanel>,
+    mode: NewSessionMode,
+    stop_on_entry: ToggleState,
+    debugger: Option<SharedString>,
+    last_selected_profile_name: Option<SharedString>,
+}
+
+fn suggested_label(request: &DebugRequestType, debugger: &str) -> String {
+    match request {
+        DebugRequestType::Launch(config) => {
+            let last_path_component = Path::new(&config.program)
+                .file_name()
+                .map(|name| name.to_string_lossy())
+                .unwrap_or_else(|| Cow::Borrowed(&config.program));
+
+            format!("{} ({debugger})", last_path_component)
+        }
+        DebugRequestType::Attach(config) => format!(
+            "pid: {} ({debugger})",
+            config.process_id.unwrap_or(u32::MAX)
+        ),
+    }
+}
+
+impl NewSessionModal {
+    pub(super) fn new(
+        past_debug_definition: Option<DebugTaskDefinition>,
+        debug_panel: WeakEntity<DebugPanel>,
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Self {
+        let debugger = past_debug_definition
+            .as_ref()
+            .map(|def| def.adapter.clone().into());
+
+        let stop_on_entry = past_debug_definition
+            .as_ref()
+            .and_then(|def| def.stop_on_entry);
+
+        let launch_config = match past_debug_definition.map(|def| def.request) {
+            Some(DebugRequestType::Launch(launch_config)) => Some(launch_config),
+            _ => None,
+        };
+
+        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,
+        }
+    }
+
+    fn debug_config(&self, cx: &App) -> Option<DebugTaskDefinition> {
+        let request = self.mode.debug_task(cx);
+
+        Some(DebugTaskDefinition {
+            adapter: self.debugger.clone()?.to_string(),
+            label: suggested_label(&request, self.debugger.as_deref()?),
+            request,
+            initialize_args: None,
+            tcp_connection: None,
+            locator: None,
+            stop_on_entry: match self.stop_on_entry {
+                ToggleState::Selected => Some(true),
+                _ => None,
+            },
+        })
+    }
+    fn start_new_session(&self, cx: &mut Context<Self>) -> Result<()> {
+        let workspace = self.workspace.clone();
+        let config = self
+            .debug_config(cx)
+            .ok_or_else(|| anyhow!("Failed to create a debug config"))?;
+
+        let _ = self.debug_panel.update(cx, |panel, _| {
+            panel.past_debug_definition = Some(config.clone());
+        });
+
+        cx.spawn(async move |this, cx| {
+            let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
+            let task =
+                project.update(cx, |this, cx| this.start_debug_session(config.into(), cx))?;
+            let spawn_result = task.await;
+            if spawn_result.is_ok() {
+                this.update(cx, |_, cx| {
+                    cx.emit(DismissEvent);
+                })
+                .ok();
+            }
+            spawn_result?;
+            anyhow::Result::<_, anyhow::Error>::Ok(())
+        })
+        .detach_and_log_err(cx);
+        Ok(())
+    }
+
+    fn update_attach_picker(
+        attach: &Entity<AttachMode>,
+        selected_debugger: &str,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
+        attach.update(cx, |this, cx| {
+            if selected_debugger != this.debug_definition.adapter {
+                this.debug_definition.adapter = selected_debugger.into();
+                if let Some(project) = this
+                    .workspace
+                    .read_with(cx, |workspace, _| workspace.project().clone())
+                    .ok()
+                {
+                    this.attach_picker = Some(cx.new(|cx| {
+                        let modal = AttachModal::new(
+                            project,
+                            this.debug_definition.clone(),
+                            false,
+                            window,
+                            cx,
+                        );
+
+                        window.focus(&modal.focus_handle(cx));
+
+                        modal
+                    }));
+                }
+            }
+
+            cx.notify();
+        })
+    }
+    fn adapter_drop_down_menu(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> ui::DropdownMenu {
+        let workspace = self.workspace.clone();
+        let weak = cx.weak_entity();
+        let debugger = self.debugger.clone();
+        DropdownMenu::new(
+            "dap-adapter-picker",
+            debugger
+                .as_ref()
+                .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
+                .clone(),
+            ContextMenu::build(window, cx, move |mut menu, _, cx| {
+                let setter_for_name = |name: SharedString| {
+                    let weak = weak.clone();
+                    move |window: &mut Window, cx: &mut App| {
+                        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);
+                            }
+                        })
+                        .ok();
+                    }
+                };
+
+                let available_adapters = workspace
+                    .update(cx, |this, cx| {
+                        this.project()
+                            .read(cx)
+                            .debug_adapters()
+                            .enumerate_adapters()
+                    })
+                    .ok()
+                    .unwrap_or_default();
+
+                for adapter in available_adapters {
+                    menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.0.clone()));
+                }
+                menu
+            }),
+        )
+    }
+
+    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();
+        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: DebugTaskDefinition| {
+                    let weak = weak.clone();
+                    let workspace = workspace.clone();
+                    move |window: &mut Window, cx: &mut App| {
+                        weak.update(cx, |this, cx| {
+                            this.last_selected_profile_name = Some(SharedString::from(&task.label));
+                            this.debugger = Some(task.adapter.clone().into());
+
+                            match &task.request {
+                                DebugRequestType::Launch(launch_config) => {
+                                    this.mode = NewSessionMode::launch(
+                                        Some(launch_config.clone()),
+                                        window,
+                                        cx,
+                                    );
+                                }
+                                DebugRequestType::Attach(_) => {
+                                    this.mode = NewSessionMode::attach(
+                                        this.debugger.clone(),
+                                        workspace.clone(),
+                                        window,
+                                        cx,
+                                    );
+                                    if let Some((debugger, attach)) =
+                                        this.debugger.as_ref().zip(this.mode.as_attach())
+                                    {
+                                        Self::update_attach_picker(&attach, &debugger, window, cx);
+                                    }
+                                }
+                            }
+                            cx.notify();
+                        })
+                        .ok();
+                    }
+                };
+
+                let available_adapters: Vec<DebugTaskDefinition> = 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_tasks())
+                            .cloned()
+                            .filter_map(|task| task.try_into().ok())
+                            .collect()
+                    })
+                    .ok()
+                    .unwrap_or_default();
+
+                for debug_definition in available_adapters {
+                    menu = menu.entry(
+                        debug_definition.label.clone(),
+                        None,
+                        setter_for_name(debug_definition),
+                    );
+                }
+                menu
+            }),
+        )
+    }
+}
+
+#[derive(Clone)]
+struct LaunchMode {
+    program: Entity<Editor>,
+    cwd: Entity<Editor>,
+}
+
+impl LaunchMode {
+    fn new(
+        past_launch_config: Option<LaunchConfig>,
+        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));
+
+        let program = cx.new(|cx| Editor::single_line(window, cx));
+        program.update(cx, |this, cx| {
+            this.set_placeholder_text("Program path", cx);
+
+            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 })
+    }
+
+    fn debug_task(&self, cx: &App) -> task::LaunchConfig {
+        let path = self.cwd.read(cx).text(cx);
+        task::LaunchConfig {
+            program: self.program.read(cx).text(cx),
+            cwd: path.is_empty().not().then(|| PathBuf::from(path)),
+            args: Default::default(),
+        }
+    }
+}
+
+#[derive(Clone)]
+struct AttachMode {
+    workspace: WeakEntity<Workspace>,
+    debug_definition: DebugTaskDefinition,
+    attach_picker: Option<Entity<AttachModal>>,
+    focus_handle: FocusHandle,
+}
+
+impl AttachMode {
+    fn new(
+        debugger: Option<SharedString>,
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Entity<Self> {
+        let debug_definition = DebugTaskDefinition {
+            label: "Attach New Session Setup".into(),
+            request: dap::DebugRequestType::Attach(task::AttachConfig { process_id: None }),
+            tcp_connection: None,
+            adapter: debugger.clone().unwrap_or_default().into(),
+            locator: None,
+            initialize_args: None,
+            stop_on_entry: Some(false),
+        };
+
+        let attach_picker = if let Some(project) = debugger.and(
+            workspace
+                .read_with(cx, |workspace, _| workspace.project().clone())
+                .ok(),
+        ) {
+            Some(cx.new(|cx| {
+                let modal = AttachModal::new(project, debug_definition.clone(), false, window, cx);
+                window.focus(&modal.focus_handle(cx));
+
+                modal
+            }))
+        } else {
+            None
+        };
+
+        cx.new(|cx| Self {
+            workspace,
+            debug_definition,
+            attach_picker,
+            focus_handle: cx.focus_handle(),
+        })
+    }
+    fn debug_task(&self) -> task::AttachConfig {
+        task::AttachConfig { process_id: None }
+    }
+}
+
+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>),
+    Attach(Entity<AttachMode>),
+}
+
+impl NewSessionMode {
+    fn debug_task(&self, cx: &App) -> DebugRequestType {
+        match self {
+            NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
+            NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
+        }
+    }
+    fn as_attach(&self) -> Option<&Entity<AttachMode>> {
+        if let NewSessionMode::Attach(entity) = self {
+            Some(entity)
+        } else {
+            None
+        }
+    }
+}
+
+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).focus_handle.clone(),
+        }
+    }
+}
+
+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, _: &mut App) -> impl IntoElement {
+        v_flex().w_full().children(self.attach_picker.clone())
+    }
+}
+
+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()
+            }),
+        }
+    }
+}
+
+impl NewSessionMode {
+    fn attach(
+        debugger: Option<SharedString>,
+        workspace: WeakEntity<Workspace>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Self {
+        Self::Attach(AttachMode::new(debugger, workspace, window, cx))
+    }
+    fn launch(past_launch_config: Option<LaunchConfig>, window: &mut Window, cx: &mut App) -> Self {
+        Self::Launch(LaunchMode::new(past_launch_config, window, cx))
+    }
+}
+fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
+    let settings = ThemeSettings::get_global(cx);
+    let theme = cx.theme();
+
+    let text_style = TextStyle {
+        color: cx.theme().colors().text,
+        font_family: settings.buffer_font.family.clone(),
+        font_features: settings.buffer_font.features.clone(),
+        font_size: settings.buffer_font_size(cx).into(),
+        font_weight: settings.buffer_font.weight,
+        line_height: relative(settings.buffer_line_height.value()),
+        background_color: Some(theme.colors().editor_background),
+        ..Default::default()
+    };
+
+    let element = EditorElement::new(
+        editor,
+        EditorStyle {
+            background: theme.colors().editor_background,
+            local_player: theme.players().local(),
+            text: text_style,
+            ..Default::default()
+        },
+    );
+
+    div()
+        .rounded_md()
+        .p_1()
+        .border_1()
+        .border_color(theme.colors().border_variant)
+        .when(
+            editor.focus_handle(cx).contains_focused(window, cx),
+            |this| this.border_color(theme.colors().border_focused),
+        )
+        .child(element)
+        .bg(theme.colors().editor_background)
+}
+
+impl Render for NewSessionModal {
+    fn render(
+        &mut self,
+        window: &mut ui::Window,
+        cx: &mut ui::Context<Self>,
+    ) -> impl ui::IntoElement {
+        v_flex()
+            .size_full()
+            .w(rems(34.))
+            .elevation_3(cx)
+            .bg(cx.theme().colors().elevated_surface_background)
+            .on_action(cx.listener(|_, _: &menu::Cancel, _, cx| {
+                cx.emit(DismissEvent);
+            }))
+            .child(
+                h_flex()
+                    .w_full()
+                    .justify_around()
+                    .p_2()
+                    .child(
+                        h_flex()
+                            .justify_start()
+                            .w_full()
+                            .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();
+                                }))
+                                .first(),
+                            )
+                            .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| {
+                                    this.mode = NewSessionMode::attach(
+                                        this.debugger.clone(),
+                                        this.workspace.clone(),
+                                        window,
+                                        cx,
+                                    );
+                                    if let Some((debugger, attach)) =
+                                        this.debugger.as_ref().zip(this.mode.as_attach())
+                                    {
+                                        Self::update_attach_picker(&attach, &debugger, window, cx);
+                                    }
+                                    this.mode.focus_handle(cx).focus(window);
+                                    cx.notify();
+                                }))
+                                .last(),
+                            ),
+                    )
+                    .justify_between()
+                    .child(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(
+                h_flex()
+                    .justify_between()
+                    .gap_2()
+                    .p_2()
+                    .border_color(cx.theme().colors().border_variant)
+                    .border_t_1()
+                    .w_full()
+                    .child(self.debug_config_drop_down_menu(window, cx))
+                    .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, _, _, cx| {
+                                        this.start_new_session(cx).log_err();
+                                    }))
+                                    .disabled(self.debugger.is_none()),
+                            ),
+                    ),
+            )
+    }
+}
+
+impl EventEmitter<DismissEvent> for NewSessionModal {}
+impl Focusable for NewSessionModal {
+    fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
+        self.mode.focus_handle(cx)
+    }
+}
+
+impl ModalView for NewSessionModal {}

crates/debugger_ui/src/session.rs 🔗

@@ -1,26 +1,13 @@
-mod failed;
-mod inert;
 pub mod running;
-mod starting;
-
-use std::time::Duration;
 
 use dap::client::SessionId;
-use failed::FailedState;
-use gpui::{
-    Animation, AnimationExt, AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable,
-    Subscription, Task, Transformation, WeakEntity, percentage,
-};
-use inert::{InertEvent, InertState};
+use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
 use project::Project;
 use project::debugger::{dap_store::DapStore, session::Session};
 use project::worktree_store::WorktreeStore;
 use rpc::proto::{self, PeerId};
 use running::RunningState;
-use starting::{StartingEvent, StartingState};
-use task::DebugTaskDefinition;
-use ui::{Indicator, prelude::*};
-use util::ResultExt;
+use ui::prelude::*;
 use workspace::{
     FollowableItem, ViewId, Workspace,
     item::{self, Item},
@@ -29,9 +16,6 @@ use workspace::{
 use crate::debugger_panel::DebugPanel;
 
 pub(crate) enum DebugSessionState {
-    Inert(Entity<InertState>),
-    Starting(Entity<StartingState>),
-    Failed(Entity<FailedState>),
     Running(Entity<running::RunningState>),
 }
 
@@ -39,7 +23,6 @@ impl DebugSessionState {
     pub(crate) fn as_running(&self) -> Option<&Entity<running::RunningState>> {
         match &self {
             DebugSessionState::Running(entity) => Some(entity),
-            _ => None,
         }
     }
 }
@@ -48,9 +31,9 @@ pub struct DebugSession {
     remote_id: Option<workspace::ViewId>,
     mode: DebugSessionState,
     dap_store: WeakEntity<DapStore>,
-    debug_panel: WeakEntity<DebugPanel>,
-    worktree_store: WeakEntity<WorktreeStore>,
-    workspace: WeakEntity<Workspace>,
+    _debug_panel: WeakEntity<DebugPanel>,
+    _worktree_store: WeakEntity<WorktreeStore>,
+    _workspace: WeakEntity<Workspace>,
     _subscriptions: [Subscription; 1],
 }
 
@@ -69,46 +52,11 @@ pub enum ThreadItem {
 }
 
 impl DebugSession {
-    pub(super) fn inert(
-        project: Entity<Project>,
-        workspace: WeakEntity<Workspace>,
-        debug_panel: WeakEntity<DebugPanel>,
-        config: Option<DebugTaskDefinition>,
-        window: &mut Window,
-        cx: &mut App,
-    ) -> Entity<Self> {
-        let default_cwd = project
-            .read(cx)
-            .worktrees(cx)
-            .next()
-            .and_then(|tree| tree.read(cx).abs_path().to_str().map(|str| str.to_string()))
-            .unwrap_or_default();
-
-        let inert =
-            cx.new(|cx| InertState::new(workspace.clone(), &default_cwd, config, window, cx));
-
-        let project = project.read(cx);
-        let dap_store = project.dap_store().downgrade();
-        let worktree_store = project.worktree_store().downgrade();
-        cx.new(|cx| {
-            let _subscriptions = [cx.subscribe_in(&inert, window, Self::on_inert_event)];
-            Self {
-                remote_id: None,
-                mode: DebugSessionState::Inert(inert),
-                dap_store,
-                worktree_store,
-                debug_panel,
-                workspace,
-                _subscriptions,
-            }
-        })
-    }
-
     pub(crate) fn running(
         project: Entity<Project>,
         workspace: WeakEntity<Workspace>,
         session: Entity<Session>,
-        debug_panel: WeakEntity<DebugPanel>,
+        _debug_panel: WeakEntity<DebugPanel>,
         window: &mut Window,
         cx: &mut App,
     ) -> Entity<Self> {
@@ -121,26 +69,20 @@ impl DebugSession {
             remote_id: None,
             mode: DebugSessionState::Running(mode),
             dap_store: project.read(cx).dap_store().downgrade(),
-            debug_panel,
-            worktree_store: project.read(cx).worktree_store().downgrade(),
-            workspace,
+            _debug_panel,
+            _worktree_store: project.read(cx).worktree_store().downgrade(),
+            _workspace: workspace,
         })
     }
 
     pub(crate) fn session_id(&self, cx: &App) -> Option<SessionId> {
         match &self.mode {
-            DebugSessionState::Inert(_) => None,
-            DebugSessionState::Starting(entity) => Some(entity.read(cx).session_id),
-            DebugSessionState::Failed(_) => None,
             DebugSessionState::Running(entity) => Some(entity.read(cx).session_id()),
         }
     }
 
     pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
         match &self.mode {
-            DebugSessionState::Inert(_) => {}
-            DebugSessionState::Starting(_entity) => {} // todo(debugger): we need to shutdown the starting process in this case (or recreate it on a breakpoint being hit)
-            DebugSessionState::Failed(_) => {}
             DebugSessionState::Running(state) => state.update(cx, |state, cx| state.shutdown(cx)),
         }
     }
@@ -149,63 +91,29 @@ impl DebugSession {
         &self.mode
     }
 
-    fn on_inert_event(
-        &mut self,
-        _: &Entity<InertState>,
-        event: &InertEvent,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let dap_store = self.dap_store.clone();
-        let InertEvent::Spawned { config } = event;
-        let config = config.clone();
-
-        self.debug_panel
-            .update(cx, |this, _| this.last_inert_config = Some(config.clone()))
-            .log_err();
-
-        let worktree = self
-            .worktree_store
-            .update(cx, |this, _| this.worktrees().next())
-            .ok()
-            .flatten()
-            .expect("worktree-less project");
-        let Ok((new_session_id, task)) = dap_store.update(cx, |store, cx| {
-            store.new_session(config.into(), &worktree, None, cx)
-        }) else {
-            return;
+    pub(crate) fn label(&self, cx: &App) -> String {
+        let session_id = match &self.mode {
+            DebugSessionState::Running(running_state) => running_state.read(cx).session_id(),
         };
-        let starting = cx.new(|cx| StartingState::new(new_session_id, task, cx));
-
-        self._subscriptions = [cx.subscribe_in(&starting, window, Self::on_starting_event)];
-        self.mode = DebugSessionState::Starting(starting);
-    }
-
-    fn on_starting_event(
-        &mut self,
-        _: &Entity<StartingState>,
-        event: &StartingEvent,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let StartingEvent::Finished(session) = event {
-            let mode =
-                cx.new(|cx| RunningState::new(session.clone(), self.workspace.clone(), window, cx));
-            self.mode = DebugSessionState::Running(mode);
-        } else if let StartingEvent::Failed = event {
-            self.mode = DebugSessionState::Failed(cx.new(FailedState::new));
+        let Ok(Some(session)) = self
+            .dap_store
+            .read_with(cx, |store, _| store.session_by_id(session_id))
+        else {
+            return "".to_owned();
         };
-        cx.notify();
+        session
+            .read(cx)
+            .as_local()
+            .expect("Remote Debug Sessions are not implemented yet")
+            .label()
     }
 }
+
 impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
 
 impl Focusable for DebugSession {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         match &self.mode {
-            DebugSessionState::Inert(inert_state) => inert_state.focus_handle(cx),
-            DebugSessionState::Starting(starting_state) => starting_state.focus_handle(cx),
-            DebugSessionState::Failed(failed_state) => failed_state.focus_handle(cx),
             DebugSessionState::Running(running_state) => running_state.focus_handle(cx),
         }
     }
@@ -213,61 +121,6 @@ impl Focusable for DebugSession {
 
 impl Item for DebugSession {
     type Event = DebugPanelItemEvent;
-    fn tab_content(&self, _: item::TabContentParams, _: &Window, cx: &App) -> AnyElement {
-        let (icon, label, color) = match &self.mode {
-            DebugSessionState::Inert(_) => (None, "New Session", Color::Default),
-            DebugSessionState::Starting(_) => (None, "Starting", Color::Default),
-            DebugSessionState::Failed(_) => (
-                Some(Indicator::dot().color(Color::Error)),
-                "Failed",
-                Color::Error,
-            ),
-            DebugSessionState::Running(state) => {
-                if state.read(cx).session().read(cx).is_terminated() {
-                    (
-                        Some(Indicator::dot().color(Color::Error)),
-                        "Terminated",
-                        Color::Error,
-                    )
-                } else {
-                    match state.read(cx).thread_status(cx).unwrap_or_default() {
-                        project::debugger::session::ThreadStatus::Stopped => (
-                            Some(Indicator::dot().color(Color::Conflict)),
-                            state
-                                .read_with(cx, |state, cx| state.thread_status(cx))
-                                .map(|status| status.label())
-                                .unwrap_or("Stopped"),
-                            Color::Conflict,
-                        ),
-                        _ => (
-                            Some(Indicator::dot().color(Color::Success)),
-                            state
-                                .read_with(cx, |state, cx| state.thread_status(cx))
-                                .map(|status| status.label())
-                                .unwrap_or("Running"),
-                            Color::Success,
-                        ),
-                    }
-                }
-            }
-        };
-
-        let is_starting = matches!(self.mode, DebugSessionState::Starting(_));
-
-        h_flex()
-            .gap_2()
-            .children(is_starting.then(|| {
-                Icon::new(IconName::ArrowCircle).with_animation(
-                    "starting-debug-session",
-                    Animation::new(Duration::from_secs(2)).repeat(),
-                    |this, delta| this.transform(Transformation::rotate(percentage(delta))),
-                )
-            }))
-            .when_some(icon, |this, indicator| this.child(indicator))
-            .justify_between()
-            .child(Label::new(label).color(color))
-            .into_any_element()
-    }
 }
 
 impl FollowableItem for DebugSession {
@@ -339,15 +192,6 @@ impl FollowableItem for DebugSession {
 impl Render for DebugSession {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         match &self.mode {
-            DebugSessionState::Inert(inert_state) => {
-                inert_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
-            }
-            DebugSessionState::Starting(starting_state) => {
-                starting_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
-            }
-            DebugSessionState::Failed(failed_state) => {
-                failed_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
-            }
             DebugSessionState::Running(running_state) => {
                 running_state.update(cx, |this, cx| this.render(window, cx).into_any_element())
             }

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

@@ -1,30 +0,0 @@
-use gpui::{FocusHandle, Focusable};
-use ui::{
-    Color, Context, IntoElement, Label, LabelCommon, ParentElement, Render, Styled, Window, h_flex,
-};
-
-pub(crate) struct FailedState {
-    focus_handle: FocusHandle,
-}
-impl FailedState {
-    pub(super) fn new(cx: &mut Context<Self>) -> Self {
-        Self {
-            focus_handle: cx.focus_handle(),
-        }
-    }
-}
-
-impl Focusable for FailedState {
-    fn focus_handle(&self, _: &ui::App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-impl Render for FailedState {
-    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
-        h_flex()
-            .size_full()
-            .items_center()
-            .justify_center()
-            .child(Label::new("Failed to spawn debugging session").color(Color::Error))
-    }
-}

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

@@ -1,337 +0,0 @@
-use std::path::PathBuf;
-
-use dap::DebugRequestType;
-use editor::{Editor, EditorElement, EditorStyle};
-use gpui::{App, AppContext, Entity, EventEmitter, FocusHandle, Focusable, TextStyle, WeakEntity};
-use settings::Settings as _;
-use task::{DebugTaskDefinition, LaunchConfig, TCPHost};
-use theme::ThemeSettings;
-use ui::{
-    ActiveTheme as _, ButtonCommon, ButtonLike, Clickable, Context, ContextMenu, Disableable,
-    DropdownMenu, FluentBuilder, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label,
-    LabelCommon, LabelSize, ParentElement, PopoverMenu, PopoverMenuHandle, Render, SharedString,
-    SplitButton, Styled, Window, div, h_flex, relative, v_flex,
-};
-use workspace::Workspace;
-
-use crate::attach_modal::AttachModal;
-
-#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
-enum SpawnMode {
-    #[default]
-    Launch,
-    Attach,
-}
-
-impl SpawnMode {
-    fn label(&self) -> &'static str {
-        match self {
-            SpawnMode::Launch => "Launch",
-            SpawnMode::Attach => "Attach",
-        }
-    }
-}
-
-impl From<DebugRequestType> for SpawnMode {
-    fn from(request: DebugRequestType) -> Self {
-        match request {
-            DebugRequestType::Launch(_) => SpawnMode::Launch,
-            DebugRequestType::Attach(_) => SpawnMode::Attach,
-        }
-    }
-}
-
-pub(crate) struct InertState {
-    focus_handle: FocusHandle,
-    selected_debugger: Option<SharedString>,
-    program_editor: Entity<Editor>,
-    cwd_editor: Entity<Editor>,
-    workspace: WeakEntity<Workspace>,
-    spawn_mode: SpawnMode,
-    popover_handle: PopoverMenuHandle<ContextMenu>,
-}
-
-impl InertState {
-    pub(super) fn new(
-        workspace: WeakEntity<Workspace>,
-        default_cwd: &str,
-        debug_config: Option<DebugTaskDefinition>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let selected_debugger = debug_config
-            .as_ref()
-            .map(|config| SharedString::from(config.adapter.clone()));
-
-        let spawn_mode = debug_config
-            .as_ref()
-            .map(|config| config.request.clone().into())
-            .unwrap_or_default();
-
-        let program = debug_config
-            .as_ref()
-            .and_then(|config| match &config.request {
-                DebugRequestType::Attach(_) => None,
-                DebugRequestType::Launch(launch_config) => Some(launch_config.program.clone()),
-            });
-
-        let program_editor = cx.new(|cx| {
-            let mut editor = Editor::single_line(window, cx);
-            if let Some(program) = program {
-                editor.insert(&program, window, cx);
-            } else {
-                editor.set_placeholder_text("Program path", cx);
-            }
-            editor
-        });
-
-        let cwd = debug_config
-            .and_then(|config| match &config.request {
-                DebugRequestType::Attach(_) => None,
-                DebugRequestType::Launch(launch_config) => launch_config.cwd.clone(),
-            })
-            .unwrap_or_else(|| PathBuf::from(default_cwd));
-
-        let cwd_editor = cx.new(|cx| {
-            let mut editor = Editor::single_line(window, cx);
-            editor.insert(cwd.to_str().unwrap_or_else(|| default_cwd), window, cx);
-            editor.set_placeholder_text("Working directory", cx);
-            editor
-        });
-
-        Self {
-            workspace,
-            cwd_editor,
-            program_editor,
-            selected_debugger,
-            spawn_mode,
-            focus_handle: cx.focus_handle(),
-            popover_handle: Default::default(),
-        }
-    }
-}
-impl Focusable for InertState {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-pub(crate) enum InertEvent {
-    Spawned { config: DebugTaskDefinition },
-}
-
-impl EventEmitter<InertEvent> for InertState {}
-
-static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
-
-impl Render for InertState {
-    fn render(
-        &mut self,
-        window: &mut ui::Window,
-        cx: &mut ui::Context<'_, Self>,
-    ) -> impl ui::IntoElement {
-        let weak = cx.weak_entity();
-        let workspace = self.workspace.clone();
-        let disable_buttons = self.selected_debugger.is_none();
-        let spawn_button = ButtonLike::new_rounded_left("spawn-debug-session")
-            .child(Label::new(self.spawn_mode.label()).size(LabelSize::Small))
-            .on_click(cx.listener(|this, _, window, cx| {
-                if this.spawn_mode == SpawnMode::Launch {
-                    let program = this.program_editor.read(cx).text(cx);
-                    let cwd = PathBuf::from(this.cwd_editor.read(cx).text(cx));
-                    let kind = this
-                        .selected_debugger
-                        .as_deref()
-                        .unwrap_or_else(|| {
-                            unimplemented!(
-                                "Automatic selection of a debugger based on users project"
-                            )
-                        })
-                        .to_string();
-
-                    cx.emit(InertEvent::Spawned {
-                        config: DebugTaskDefinition {
-                            label: "hard coded".into(),
-                            adapter: kind,
-                            request: DebugRequestType::Launch(LaunchConfig {
-                                program,
-                                cwd: Some(cwd),
-                                args: Default::default(),
-                            }),
-                            tcp_connection: Some(TCPHost::default()),
-                            initialize_args: None,
-                            locator: None,
-                            stop_on_entry: None,
-                        },
-                    });
-                } else {
-                    this.attach(window, cx)
-                }
-            }))
-            .disabled(disable_buttons);
-
-        v_flex()
-            .track_focus(&self.focus_handle)
-            .size_full()
-            .gap_1()
-            .p_2()
-            .child(
-                v_flex()
-                    .gap_1()
-                    .child(
-                        h_flex()
-                            .w_full()
-                            .gap_2()
-                            .child(Self::render_editor(&self.program_editor, cx))
-                            .child(
-                                h_flex().child(DropdownMenu::new(
-                                    "dap-adapter-picker",
-                                    self.selected_debugger
-                                        .as_ref()
-                                        .unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
-                                        .clone(),
-                                    ContextMenu::build(window, cx, move |mut this, _, cx| {
-                                        let setter_for_name = |name: SharedString| {
-                                            let weak = weak.clone();
-                                            move |_: &mut Window, cx: &mut App| {
-                                                let name = name.clone();
-                                                weak.update(cx, move |this, cx| {
-                                                    this.selected_debugger = Some(name.clone());
-                                                    cx.notify();
-                                                })
-                                                .ok();
-                                            }
-                                        };
-                                        let available_adapters = workspace
-                                            .update(cx, |this, cx| {
-                                                this.project()
-                                                    .read(cx)
-                                                    .debug_adapters()
-                                                    .enumerate_adapters()
-                                            })
-                                            .ok()
-                                            .unwrap_or_default();
-
-                                        for adapter in available_adapters {
-                                            this = this.entry(
-                                                adapter.0.clone(),
-                                                None,
-                                                setter_for_name(adapter.0.clone()),
-                                            );
-                                        }
-                                        this
-                                    }),
-                                )),
-                            ),
-                    )
-                    .child(
-                        h_flex()
-                            .gap_2()
-                            .child(Self::render_editor(&self.cwd_editor, cx))
-                            .map(|this| {
-                                let entity = cx.weak_entity();
-                                this.child(SplitButton {
-                                    left: spawn_button,
-                                    right: PopoverMenu::new("debugger-select-spawn-mode")
-                                        .trigger(
-                                            ButtonLike::new_rounded_right(
-                                                "debugger-spawn-button-mode",
-                                            )
-                                            .layer(ui::ElevationIndex::ModalSurface)
-                                            .size(ui::ButtonSize::None)
-                                            .child(
-                                                div().px_1().child(
-                                                    Icon::new(IconName::ChevronDownSmall)
-                                                        .size(IconSize::XSmall),
-                                                ),
-                                            ),
-                                        )
-                                        .menu(move |window, cx| {
-                                            Some(ContextMenu::build(window, cx, {
-                                                let entity = entity.clone();
-                                                move |this, _, _| {
-                                                    this.entry("Launch", None, {
-                                                        let entity = entity.clone();
-                                                        move |_, cx| {
-                                                            let _ =
-                                                                entity.update(cx, |this, cx| {
-                                                                    this.spawn_mode =
-                                                                        SpawnMode::Launch;
-                                                                    cx.notify();
-                                                                });
-                                                        }
-                                                    })
-                                                    .entry("Attach", None, {
-                                                        let entity = entity.clone();
-                                                        move |_, cx| {
-                                                            let _ =
-                                                                entity.update(cx, |this, cx| {
-                                                                    this.spawn_mode =
-                                                                        SpawnMode::Attach;
-                                                                    cx.notify();
-                                                                });
-                                                        }
-                                                    })
-                                                }
-                                            }))
-                                        })
-                                        .with_handle(self.popover_handle.clone())
-                                        .into_any_element(),
-                                })
-                            }),
-                    ),
-            )
-    }
-}
-
-impl InertState {
-    fn render_editor(editor: &Entity<Editor>, cx: &Context<Self>) -> impl IntoElement {
-        let settings = ThemeSettings::get_global(cx);
-        let text_style = TextStyle {
-            color: cx.theme().colors().text,
-            font_family: settings.buffer_font.family.clone(),
-            font_features: settings.buffer_font.features.clone(),
-            font_size: settings.buffer_font_size(cx).into(),
-            font_weight: settings.buffer_font.weight,
-            line_height: relative(settings.buffer_line_height.value()),
-            ..Default::default()
-        };
-
-        EditorElement::new(
-            editor,
-            EditorStyle {
-                background: cx.theme().colors().editor_background,
-                local_player: cx.theme().players().local(),
-                text: text_style,
-                ..Default::default()
-            },
-        )
-    }
-
-    fn attach(&self, window: &mut Window, cx: &mut Context<Self>) {
-        let kind = self
-            .selected_debugger
-            .as_deref()
-            .map(|s| s.to_string())
-            .unwrap_or_else(|| {
-                unimplemented!("Automatic selection of a debugger based on users project")
-            });
-
-        let config = DebugTaskDefinition {
-            label: "hard coded attach".into(),
-            adapter: kind,
-            request: DebugRequestType::Attach(task::AttachConfig { process_id: None }),
-            initialize_args: None,
-            locator: None,
-            tcp_connection: Some(TCPHost::default()),
-            stop_on_entry: None,
-        };
-
-        let _ = self.workspace.update(cx, |workspace, cx| {
-            let project = workspace.project().clone();
-            workspace.toggle_modal(window, cx, |window, cx| {
-                AttachModal::new(project, config, window, cx)
-            });
-        });
-    }
-}

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

@@ -15,10 +15,9 @@ use rpc::proto::ViewId;
 use settings::Settings;
 use stack_frame_list::StackFrameList;
 use ui::{
-    ActiveTheme, AnyElement, App, Button, ButtonCommon, Clickable, Context, ContextMenu,
-    Disableable, Divider, DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, Indicator,
-    InteractiveElement, IntoElement, Label, ParentElement, Render, SharedString,
-    StatefulInteractiveElement, Styled, Tooltip, Window, div, h_flex, v_flex,
+    ActiveTheme, AnyElement, App, Button, Context, ContextMenu, DropdownMenu, FluentBuilder,
+    Indicator, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
+    StatefulInteractiveElement, Styled, Window, div, h_flex, v_flex,
 };
 use util::ResultExt;
 use variable_list::VariableList;
@@ -42,7 +41,7 @@ pub struct RunningState {
 }
 
 impl Render for RunningState {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let threads = self.session.update(cx, |this, cx| this.threads(cx));
         self.select_current_thread(&threads, cx);
 
@@ -51,255 +50,27 @@ impl Render for RunningState {
             .map(|thread_id| self.session.read(cx).thread_status(thread_id))
             .unwrap_or(ThreadStatus::Exited);
 
-        let selected_thread_name = threads
-            .iter()
-            .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
-            .map(|(thread, _)| thread.name.clone())
-            .unwrap_or("Threads".to_owned());
-
         self.variable_list.update(cx, |this, cx| {
             this.disabled(thread_status != ThreadStatus::Stopped, cx);
         });
 
         let active_thread_item = &self.active_thread_item;
 
-        let has_no_threads = threads.is_empty();
         let capabilities = self.capabilities(cx);
-        let state = cx.entity();
         h_flex()
             .key_context("DebugPanelItem")
             .track_focus(&self.focus_handle(cx))
             .size_full()
             .items_start()
             .child(
-                v_flex()
-                    .size_full()
-                    .items_start()
-                    .child(
-                        h_flex()
-                            .w_full()
-                            .border_b_1()
-                            .border_color(cx.theme().colors().border_variant)
-                            .justify_between()
-                            .child(
-                                h_flex()
-                                    .px_1()
-                                    .py_0p5()
-                                    .w_full()
-                                    .gap_1()
-                                    .map(|this| {
-                                        if thread_status == ThreadStatus::Running {
-                                            this.child(
-                                                IconButton::new(
-                                                    "debug-pause",
-                                                    IconName::DebugPause,
-                                                )
-                                                .icon_size(IconSize::XSmall)
-                                                .on_click(cx.listener(|this, _, _window, cx| {
-                                                    this.pause_thread(cx);
-                                                }))
-                                                .tooltip(move |window, cx| {
-                                                    Tooltip::text("Pause program")(window, cx)
-                                                }),
-                                            )
-                                        } else {
-                                            this.child(
-                                                IconButton::new(
-                                                    "debug-continue",
-                                                    IconName::DebugContinue,
-                                                )
-                                                .icon_size(IconSize::XSmall)
-                                                .on_click(cx.listener(|this, _, _window, cx| {
-                                                    this.continue_thread(cx)
-                                                }))
-                                                .disabled(thread_status != ThreadStatus::Stopped)
-                                                .tooltip(move |window, cx| {
-                                                    Tooltip::text("Continue program")(window, cx)
-                                                }),
-                                            )
-                                        }
-                                    })
-                                    .child(
-                                        IconButton::new("debug-restart", IconName::DebugRestart)
-                                            .icon_size(IconSize::XSmall)
-                                            .on_click(cx.listener(|this, _, _window, cx| {
-                                                this.restart_session(cx);
-                                            }))
-                                            .disabled(
-                                                !capabilities
-                                                    .supports_restart_request
-                                                    .unwrap_or_default(),
-                                            )
-                                            .tooltip(move |window, cx| {
-                                                Tooltip::text("Restart")(window, cx)
-                                            }),
-                                    )
-                                    .child(
-                                        IconButton::new("debug-stop", IconName::DebugStop)
-                                            .icon_size(IconSize::XSmall)
-                                            .on_click(cx.listener(|this, _, _window, cx| {
-                                                this.stop_thread(cx);
-                                            }))
-                                            .disabled(
-                                                thread_status != ThreadStatus::Stopped
-                                                    && thread_status != ThreadStatus::Running,
-                                            )
-                                            .tooltip({
-                                                let label = if capabilities
-                                                    .supports_terminate_threads_request
-                                                    .unwrap_or_default()
-                                                {
-                                                    "Terminate Thread"
-                                                } else {
-                                                    "Terminate all Threads"
-                                                };
-                                                move |window, cx| Tooltip::text(label)(window, cx)
-                                            }),
-                                    )
-                                    .child(
-                                        IconButton::new(
-                                            "debug-disconnect",
-                                            IconName::DebugDisconnect,
-                                        )
-                                        .icon_size(IconSize::XSmall)
-                                        .on_click(cx.listener(|this, _, _window, cx| {
-                                            this.disconnect_client(cx);
-                                        }))
-                                        .disabled(
-                                            thread_status == ThreadStatus::Exited
-                                                || thread_status == ThreadStatus::Ended,
-                                        )
-                                        .tooltip(Tooltip::text("Disconnect")),
-                                    )
-                                    .child(Divider::vertical())
-                                    .when(
-                                        capabilities.supports_step_back.unwrap_or(false),
-                                        |this| {
-                                            this.child(
-                                                IconButton::new(
-                                                    "debug-step-back",
-                                                    IconName::DebugStepBack,
-                                                )
-                                                .icon_size(IconSize::XSmall)
-                                                .on_click(cx.listener(|this, _, _window, cx| {
-                                                    this.step_back(cx);
-                                                }))
-                                                .disabled(thread_status != ThreadStatus::Stopped)
-                                                .tooltip(move |window, cx| {
-                                                    Tooltip::text("Step back")(window, cx)
-                                                }),
-                                            )
-                                        },
-                                    )
-                                    .child(
-                                        IconButton::new("debug-step-over", IconName::DebugStepOver)
-                                            .icon_size(IconSize::XSmall)
-                                            .on_click(cx.listener(|this, _, _window, cx| {
-                                                this.step_over(cx);
-                                            }))
-                                            .disabled(thread_status != ThreadStatus::Stopped)
-                                            .tooltip(move |window, cx| {
-                                                Tooltip::text("Step over")(window, cx)
-                                            }),
-                                    )
-                                    .child(
-                                        IconButton::new("debug-step-in", IconName::DebugStepInto)
-                                            .icon_size(IconSize::XSmall)
-                                            .on_click(cx.listener(|this, _, _window, cx| {
-                                                this.step_in(cx);
-                                            }))
-                                            .disabled(thread_status != ThreadStatus::Stopped)
-                                            .tooltip(move |window, cx| {
-                                                Tooltip::text("Step in")(window, cx)
-                                            }),
-                                    )
-                                    .child(
-                                        IconButton::new("debug-step-out", IconName::DebugStepOut)
-                                            .icon_size(IconSize::XSmall)
-                                            .on_click(cx.listener(|this, _, _window, cx| {
-                                                this.step_out(cx);
-                                            }))
-                                            .disabled(thread_status != ThreadStatus::Stopped)
-                                            .tooltip(move |window, cx| {
-                                                Tooltip::text("Step out")(window, cx)
-                                            }),
-                                    )
-                                    .child(Divider::vertical())
-                                    .child(
-                                        IconButton::new(
-                                            "debug-ignore-breakpoints",
-                                            if self.session.read(cx).breakpoints_enabled() {
-                                                IconName::DebugBreakpoint
-                                            } else {
-                                                IconName::DebugIgnoreBreakpoints
-                                            },
-                                        )
-                                        .icon_size(IconSize::XSmall)
-                                        .on_click(cx.listener(|this, _, _window, cx| {
-                                            this.toggle_ignore_breakpoints(cx);
-                                        }))
-                                        .disabled(
-                                            thread_status == ThreadStatus::Exited
-                                                || thread_status == ThreadStatus::Ended,
-                                        )
-                                        .tooltip(
-                                            move |window, cx| {
-                                                Tooltip::text("Ignore breakpoints")(window, cx)
-                                            },
-                                        ),
-                                    ),
-                            )
-                            .child(
-                                h_flex()
-                                    .px_1()
-                                    .py_0p5()
-                                    .gap_2()
-                                    .w_3_4()
-                                    .justify_end()
-                                    .child(Label::new("Thread:"))
-                                    .child(
-                                        DropdownMenu::new(
-                                            ("thread-list", self.session_id.0),
-                                            selected_thread_name,
-                                            ContextMenu::build(
-                                                window,
-                                                cx,
-                                                move |mut this, _, _| {
-                                                    for (thread, _) in threads {
-                                                        let state = state.clone();
-                                                        let thread_id = thread.id;
-                                                        this = this.entry(
-                                                            thread.name,
-                                                            None,
-                                                            move |_, cx| {
-                                                                state.update(cx, |state, cx| {
-                                                                    state.select_thread(
-                                                                        ThreadId(thread_id),
-                                                                        cx,
-                                                                    );
-                                                                });
-                                                            },
-                                                        );
-                                                    }
-                                                    this
-                                                },
-                                            ),
-                                        )
-                                        .disabled(
-                                            has_no_threads
-                                                || thread_status != ThreadStatus::Stopped,
-                                        ),
-                                    ),
-                            ),
-                    )
-                    .child(
-                        h_flex()
-                            .size_full()
-                            .items_start()
-                            .p_1()
-                            .gap_4()
-                            .child(self.stack_frame_list.clone()),
-                    ),
+                v_flex().size_full().items_start().child(
+                    h_flex()
+                        .size_full()
+                        .items_start()
+                        .p_1()
+                        .gap_4()
+                        .child(self.stack_frame_list.clone()),
+                ),
             )
             .child(
                 v_flex()
@@ -450,37 +221,32 @@ impl RunningState {
         self.session_id
     }
 
-    #[cfg(any(test, feature = "test-support"))]
+    #[cfg(test)]
     pub fn set_thread_item(&mut self, thread_item: ThreadItem, cx: &mut Context<Self>) {
         self.active_thread_item = thread_item;
         cx.notify()
     }
 
-    #[cfg(any(test, feature = "test-support"))]
+    #[cfg(test)]
     pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
         &self.stack_frame_list
     }
 
-    #[cfg(any(test, feature = "test-support"))]
+    #[cfg(test)]
     pub fn console(&self) -> &Entity<Console> {
         &self.console
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn module_list(&self) -> &Entity<ModuleList> {
+    #[cfg(test)]
+    pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
         &self.module_list
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn variable_list(&self) -> &Entity<VariableList> {
+    #[cfg(test)]
+    pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
         &self.variable_list
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn are_breakpoints_ignored(&self, cx: &App) -> bool {
-        self.session.read(cx).ignore_breakpoints()
-    }
-
     pub fn capabilities(&self, cx: &App) -> Capabilities {
         self.session().read(cx).capabilities().clone()
     }
@@ -504,8 +270,8 @@ impl RunningState {
         }
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn selected_thread_id(&self) -> Option<ThreadId> {
+    #[cfg(test)]
+    pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
         self.thread_id
     }
 
@@ -583,7 +349,7 @@ impl RunningState {
         });
     }
 
-    pub fn step_in(&mut self, cx: &mut Context<Self>) {
+    pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
         let Some(thread_id) = self.thread_id else {
             return;
         };
@@ -595,7 +361,7 @@ impl RunningState {
         });
     }
 
-    pub fn step_out(&mut self, cx: &mut Context<Self>) {
+    pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
         let Some(thread_id) = self.thread_id else {
             return;
         };
@@ -607,7 +373,7 @@ impl RunningState {
         });
     }
 
-    pub fn step_back(&mut self, cx: &mut Context<Self>) {
+    pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
         let Some(thread_id) = self.thread_id else {
             return;
         };
@@ -675,6 +441,10 @@ impl RunningState {
         });
     }
 
+    #[expect(
+        unused,
+        reason = "Support for disconnecting a client is not wired through yet"
+    )]
     pub fn disconnect_client(&self, cx: &mut Context<Self>) {
         self.session().update(cx, |state, cx| {
             state.disconnect_client(cx);
@@ -686,6 +456,36 @@ impl RunningState {
             session.toggle_ignore_breakpoints(cx).detach();
         });
     }
+
+    pub(crate) fn thread_dropdown(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<'_, RunningState>,
+    ) -> DropdownMenu {
+        let state = cx.entity();
+        let threads = self.session.update(cx, |this, cx| this.threads(cx));
+        let selected_thread_name = threads
+            .iter()
+            .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
+            .map(|(thread, _)| thread.name.clone())
+            .unwrap_or("Threads".to_owned());
+        DropdownMenu::new(
+            ("thread-list", self.session_id.0),
+            selected_thread_name,
+            ContextMenu::build(window, cx, move |mut this, _, _| {
+                for (thread, _) in threads {
+                    let state = state.clone();
+                    let thread_id = thread.id;
+                    this = this.entry(thread.name, None, move |_, cx| {
+                        state.update(cx, |state, cx| {
+                            state.select_thread(ThreadId(thread_id), cx);
+                        });
+                    });
+                }
+                this
+            }),
+        )
+    }
 }
 
 impl EventEmitter<DebugPanelItemEvent> for RunningState {}

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

@@ -85,16 +85,11 @@ impl Console {
         }
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn editor(&self) -> &Entity<Editor> {
+    #[cfg(test)]
+    pub(crate) fn editor(&self) -> &Entity<Editor> {
         &self.console
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn query_bar(&self) -> &Entity<Editor> {
-        &self.query_bar
-    }
-
     fn is_local(&self, cx: &Context<Self>) -> bool {
         self.session.read(cx).is_local()
     }

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

@@ -147,11 +147,9 @@ impl ModuleList {
             )
             .into_any()
     }
-}
 
-#[cfg(any(test, feature = "test-support"))]
-impl ModuleList {
-    pub fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
+    #[cfg(test)]
+    pub(crate) fn modules(&self, cx: &mut Context<Self>) -> Vec<dap::Module> {
         self.session
             .update(cx, |session, cx| session.modules(cx).to_vec())
     }

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

@@ -87,13 +87,13 @@ impl StackFrameList {
         }
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn entries(&self) -> &Vec<StackFrameEntry> {
+    #[cfg(test)]
+    pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
         &self.entries
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn flatten_entries(&self) -> Vec<dap::StackFrame> {
+    #[cfg(test)]
+    pub(crate) fn flatten_entries(&self) -> Vec<dap::StackFrame> {
         self.entries
             .iter()
             .flat_map(|frame| match frame {
@@ -115,8 +115,8 @@ impl StackFrameList {
             .unwrap_or_default()
     }
 
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
+    #[cfg(test)]
+    pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
         self.stack_frames(cx)
             .into_iter()
             .map(|stack_frame| stack_frame.dap.clone())

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

@@ -540,8 +540,8 @@ impl VariableList {
     }
 
     #[track_caller]
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn assert_visual_entries(&self, expected: Vec<&str>) {
+    #[cfg(test)]
+    pub(crate) fn assert_visual_entries(&self, expected: Vec<&str>) {
         const INDENT: &'static str = "    ";
 
         let entries = &self.entries;
@@ -569,8 +569,8 @@ impl VariableList {
     }
 
     #[track_caller]
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn scopes(&self) -> Vec<dap::Scope> {
+    #[cfg(test)]
+    pub(crate) fn scopes(&self) -> Vec<dap::Scope> {
         self.entries
             .iter()
             .filter_map(|entry| match &entry.dap_kind {
@@ -582,8 +582,8 @@ impl VariableList {
     }
 
     #[track_caller]
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
+    #[cfg(test)]
+    pub(crate) fn variables_per_scope(&self) -> Vec<(dap::Scope, Vec<dap::Variable>)> {
         let mut scopes: Vec<(dap::Scope, Vec<_>)> = Vec::new();
         let mut idx = 0;
 
@@ -604,8 +604,8 @@ impl VariableList {
     }
 
     #[track_caller]
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn variables(&self) -> Vec<dap::Variable> {
+    #[cfg(test)]
+    pub(crate) fn variables(&self) -> Vec<dap::Variable> {
         self.entries
             .iter()
             .filter_map(|entry| match &entry.dap_kind {

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

@@ -1,80 +0,0 @@
-use std::time::Duration;
-
-use anyhow::Result;
-
-use dap::client::SessionId;
-use gpui::{
-    Animation, AnimationExt, Entity, EventEmitter, FocusHandle, Focusable, Task, Transformation,
-    percentage,
-};
-use project::debugger::session::Session;
-use ui::{Color, Context, Icon, IconName, IntoElement, ParentElement, Render, Styled, v_flex};
-
-pub(crate) struct StartingState {
-    focus_handle: FocusHandle,
-    pub(super) session_id: SessionId,
-    _notify_parent: Task<()>,
-}
-
-pub(crate) enum StartingEvent {
-    Failed,
-    Finished(Entity<Session>),
-}
-
-impl EventEmitter<StartingEvent> for StartingState {}
-
-impl StartingState {
-    pub(crate) fn new(
-        session_id: SessionId,
-        task: Task<Result<Entity<Session>>>,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let _notify_parent = cx.spawn(async move |this, cx| {
-            let entity = task.await;
-
-            this.update(cx, |_, cx| {
-                if let Ok(entity) = entity {
-                    cx.emit(StartingEvent::Finished(entity))
-                } else {
-                    cx.emit(StartingEvent::Failed)
-                }
-            })
-            .ok();
-        });
-        Self {
-            session_id,
-            focus_handle: cx.focus_handle(),
-            _notify_parent,
-        }
-    }
-}
-
-impl Focusable for StartingState {
-    fn focus_handle(&self, _: &ui::App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Render for StartingState {
-    fn render(
-        &mut self,
-        _window: &mut ui::Window,
-        _cx: &mut ui::Context<'_, Self>,
-    ) -> impl ui::IntoElement {
-        v_flex()
-            .size_full()
-            .gap_1()
-            .items_center()
-            .child("Starting a debug adapter")
-            .child(
-                Icon::new(IconName::ArrowCircle)
-                    .color(Color::Info)
-                    .with_animation(
-                        "arrow-circle",
-                        Animation::new(Duration::from_secs(2)).repeat(),
-                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
-                    )
-                    .into_any_element(),
-            )
-    }
-}

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

@@ -99,8 +99,8 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
 
             debug_panel.update(cx, |this, cx| {
                 assert!(this.active_session(cx).is_some());
-                // we have one active session and one inert item
-                assert_eq!(2, this.pane().unwrap().read(cx).items_len());
+                // we have one active session
+                assert_eq!(1, this.pane().unwrap().read(cx).items_len());
                 assert!(running_state.read(cx).selected_thread_id().is_none());
             });
         })
@@ -135,9 +135,9 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
                     .clone()
             });
 
-            // we have one active session and one inert item
+            // we have one active session
             assert_eq!(
-                2,
+                1,
                 debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
             );
             assert_eq!(client.id(), running_state.read(cx).session_id());
@@ -175,7 +175,7 @@ async fn test_basic_show_debug_panel(executor: BackgroundExecutor, cx: &mut Test
 
             debug_panel.update(cx, |this, cx| {
                 assert!(this.active_session(cx).is_some());
-                assert_eq!(2, this.pane().unwrap().read(cx).items_len());
+                assert_eq!(1, this.pane().unwrap().read(cx).items_len());
                 assert_eq!(
                     ThreadId(1),
                     running_state.read(cx).selected_thread_id().unwrap()
@@ -245,8 +245,8 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
 
             debug_panel.update(cx, |this, cx| {
                 assert!(this.active_session(cx).is_some());
-                // we have one active session and one inert item
-                assert_eq!(2, this.pane().unwrap().read(cx).items_len());
+                // we have one active session
+                assert_eq!(1, this.pane().unwrap().read(cx).items_len());
             });
         })
         .unwrap();
@@ -281,9 +281,9 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
                     .clone()
             });
 
-            // we have one active session and one inert item
+            // we have one active session
             assert_eq!(
-                2,
+                1,
                 debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
             );
             assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
@@ -323,9 +323,9 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
                     .clone()
             });
 
-            // we have one active session and one inert item
+            // we have one active session
             assert_eq!(
-                2,
+                1,
                 debug_panel.update(cx, |this, cx| this.pane().unwrap().read(cx).items_len())
             );
             assert_eq!(client.id(), active_session.read(cx).session_id(cx).unwrap());
@@ -362,7 +362,7 @@ async fn test_we_can_only_have_one_panel_per_debug_session(
 
             debug_panel.update(cx, |this, cx| {
                 assert!(this.active_session(cx).is_some());
-                assert_eq!(2, this.pane().unwrap().read(cx).items_len());
+                assert_eq!(1, this.pane().unwrap().read(cx).items_len());
                 assert_eq!(
                     ThreadId(1),
                     running_state.read(cx).selected_thread_id().unwrap()
@@ -1447,7 +1447,7 @@ async fn test_unsetting_breakpoints_on_clear_breakpoint_action(
         })
         .await;
 
-    cx.dispatch_action(workspace::ClearAllBreakpoints);
+    cx.dispatch_action(crate::ClearAllBreakpoints);
     cx.run_until_parked();
 
     let shutdown_session = project.update(cx, |project, cx| {

crates/icons/src/icons.rs 🔗

@@ -23,6 +23,7 @@ pub enum IconName {
     ArrowCircle,
     ArrowDown,
     ArrowDownFromLine,
+    ArrowDownRight,
     ArrowLeft,
     ArrowRight,
     ArrowRightLeft,
@@ -44,6 +45,7 @@ pub enum IconName {
     BookCopy,
     BookPlus,
     Brain,
+    BugOff,
     CaseSensitive,
     Check,
     CheckDouble,
@@ -55,6 +57,7 @@ pub enum IconName {
     ChevronUp,
     ChevronUpDown,
     Circle,
+    CircleOff,
     Clipboard,
     Close,
     Code,
@@ -166,6 +169,7 @@ pub enum IconName {
     Play,
     Plus,
     PocketKnife,
+    Power,
     Public,
     PullRequest,
     Quote,

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

@@ -48,6 +48,7 @@ use worktree::Worktree;
 
 pub enum DapStoreEvent {
     DebugClientStarted(SessionId),
+    DebugSessionInitialized(SessionId),
     DebugClientShutdown(SessionId),
     DebugClientEvent {
         session_id: SessionId,
@@ -862,6 +863,10 @@ fn create_new_session(
             }
         }
 
+        this.update(cx, |_, cx| {
+            cx.emit(DapStoreEvent::DebugSessionInitialized(session_id));
+        })?;
+
         Ok(session)
     });
     task

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

@@ -537,7 +537,11 @@ impl LocalMode {
         Ok((adapter, binary))
     }
 
-    pub fn initialize_sequence(
+    pub fn label(&self) -> String {
+        self.config.label.clone()
+    }
+
+    fn initialize_sequence(
         &self,
         capabilities: &Capabilities,
         initialized_rx: oneshot::Receiver<()>,

crates/project/src/task_inventory.rs 🔗

@@ -125,6 +125,22 @@ impl Inventory {
         cx.new(|_| Self::default())
     }
 
+    pub fn list_debug_tasks(&self) -> Vec<&TaskTemplate> {
+        self.templates_from_settings
+            .worktree
+            .values()
+            .flat_map(|tasks| {
+                tasks.iter().filter_map(|(kind, tasks)| {
+                    if matches!(kind.1, TaskKind::Debug) {
+                        Some(tasks)
+                    } else {
+                        None
+                    }
+                })
+            })
+            .flatten()
+            .collect()
+    }
     /// Pulls its task sources relevant to the worktree and the language given,
     /// returns all task templates with their source kinds, worktree tasks first, language tasks second
     /// and global tasks last. No specific order inside source kinds groups.

crates/task/src/debug_format.rs 🔗

@@ -41,7 +41,6 @@ impl TCPHost {
 #[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)]
 pub struct AttachConfig {
     /// The processId to attach to, if left empty we will show a process picker
-    #[serde(default)]
     pub process_id: Option<u32>,
 }
 
@@ -52,7 +51,8 @@ pub struct LaunchConfig {
     pub program: String,
     /// The current working directory of your project
     pub cwd: Option<PathBuf>,
-    /// Args to pass to a debuggee
+    /// Arguments to pass to a debuggee
+    #[serde(default)]
     pub args: Vec<String>,
 }
 
@@ -66,6 +66,17 @@ pub enum DebugRequestType {
     Attach(AttachConfig),
 }
 
+impl From<LaunchConfig> for DebugRequestType {
+    fn from(launch_config: LaunchConfig) -> Self {
+        DebugRequestType::Launch(launch_config)
+    }
+}
+
+impl From<AttachConfig> for DebugRequestType {
+    fn from(attach_config: AttachConfig) -> Self {
+        DebugRequestType::Attach(attach_config)
+    }
+}
 /// Represents a request for starting the debugger.
 /// Contrary to `DebugRequestType`, `DebugRequestDisposition` is not Serializable.
 #[derive(PartialEq, Eq, Clone, Debug)]
@@ -144,6 +155,37 @@ impl TryFrom<DebugAdapterConfig> for DebugTaskDefinition {
     }
 }
 
+impl TryFrom<TaskTemplate> for DebugTaskDefinition {
+    type Error = ();
+
+    fn try_from(value: TaskTemplate) -> Result<Self, Self::Error> {
+        let TaskType::Debug(debug_args) = value.task_type else {
+            return Err(());
+        };
+
+        let request = match debug_args.request {
+            crate::DebugArgsRequest::Launch => DebugRequestType::Launch(LaunchConfig {
+                program: value.command,
+                cwd: value.cwd.map(PathBuf::from),
+                args: value.args,
+            }),
+            crate::DebugArgsRequest::Attach(attach_config) => {
+                DebugRequestType::Attach(attach_config)
+            }
+        };
+
+        Ok(DebugTaskDefinition {
+            adapter: debug_args.adapter,
+            request,
+            label: value.label,
+            initialize_args: debug_args.initialize_args,
+            tcp_connection: debug_args.tcp_connection,
+            locator: debug_args.locator,
+            stop_on_entry: debug_args.stop_on_entry,
+        })
+    }
+}
+
 impl DebugTaskDefinition {
     /// Translate from debug definition to a task template
     pub fn to_zed_format(self) -> anyhow::Result<TaskTemplate> {
@@ -249,3 +291,21 @@ impl TryFrom<DebugTaskFile> for TaskTemplates {
         Ok(Self(templates))
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::{DebugRequestType, LaunchConfig};
+
+    #[test]
+    fn test_can_deserialize_non_attach_task() {
+        let deserialized: DebugRequestType =
+            serde_json::from_str(r#"{"program": "cafebabe"}"#).unwrap();
+        assert_eq!(
+            deserialized,
+            DebugRequestType::Launch(LaunchConfig {
+                program: "cafebabe".to_owned(),
+                ..Default::default()
+            })
+        );
+    }
+}

crates/tasks_ui/src/modal.rs 🔗

@@ -338,6 +338,7 @@ impl PickerDelegate for TasksModalDelegate {
                                     debugger_ui::attach_modal::AttachModal::new(
                                         project,
                                         config.clone(),
+                                        true,
                                         window,
                                         cx,
                                     )

crates/tasks_ui/src/tasks_ui.rs 🔗

@@ -1,6 +1,7 @@
 use std::collections::HashMap;
 use std::path::Path;
 
+use debugger_ui::Start;
 use editor::Editor;
 use feature_flags::{Debugger, FeatureFlagViewExt};
 use gpui::{App, AppContext as _, Context, Entity, Task, Window};
@@ -8,7 +9,7 @@ use modal::{TaskOverrides, TasksModal};
 use project::{Location, TaskContexts, Worktree};
 use task::{RevealTarget, TaskContext, TaskId, TaskModal, TaskVariables, VariableName};
 use workspace::tasks::schedule_task;
-use workspace::{Start, Workspace, tasks::schedule_resolved_task};
+use workspace::{Workspace, tasks::schedule_resolved_task};
 
 mod modal;
 

crates/ui/src/components/button/toggle_button.rs 🔗

@@ -71,6 +71,18 @@ impl SelectableButton for ToggleButton {
     }
 }
 
+impl FixedWidth for ToggleButton {
+    fn width(mut self, width: DefiniteLength) -> Self {
+        self.base.width = Some(width);
+        self
+    }
+
+    fn full_width(mut self) -> Self {
+        self.base.width = Some(relative(1.));
+        self
+    }
+}
+
 impl Disableable for ToggleButton {
     fn disabled(mut self, disabled: bool) -> Self {
         self.base = self.base.disabled(disabled);

crates/ui/src/components/toggle.rs 🔗

@@ -253,6 +253,7 @@ pub struct CheckboxWithLabel {
     on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>,
     filled: bool,
     style: ToggleStyle,
+    checkbox_position: IconPosition,
 }
 
 // TODO: Remove `CheckboxWithLabel` now that `label` is a method of `Checkbox`.
@@ -271,6 +272,7 @@ impl CheckboxWithLabel {
             on_click: Arc::new(on_click),
             filled: false,
             style: ToggleStyle::default(),
+            checkbox_position: IconPosition::Start,
         }
     }
 
@@ -291,31 +293,51 @@ impl CheckboxWithLabel {
         self.filled = true;
         self
     }
+
+    pub fn checkbox_position(mut self, position: IconPosition) -> Self {
+        self.checkbox_position = position;
+        self
+    }
 }
 
 impl RenderOnce for CheckboxWithLabel {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
         h_flex()
             .gap(DynamicSpacing::Base08.rems(cx))
+            .when(self.checkbox_position == IconPosition::Start, |this| {
+                this.child(
+                    Checkbox::new(self.id.clone(), self.checked)
+                        .style(self.style.clone())
+                        .when(self.filled, Checkbox::fill)
+                        .on_click({
+                            let on_click = self.on_click.clone();
+                            move |checked, window, cx| {
+                                (on_click)(checked, window, cx);
+                            }
+                        }),
+                )
+            })
             .child(
-                Checkbox::new(self.id.clone(), self.checked)
-                    .style(self.style)
-                    .when(self.filled, Checkbox::fill)
+                div()
+                    .id(SharedString::from(format!("{}-label", self.id)))
                     .on_click({
                         let on_click = self.on_click.clone();
-                        move |checked, window, cx| {
-                            (on_click)(checked, window, cx);
+                        move |_event, window, cx| {
+                            (on_click)(&self.checked.inverse(), window, cx);
                         }
-                    }),
-            )
-            .child(
-                div()
-                    .id(SharedString::from(format!("{}-label", self.id)))
-                    .on_click(move |_event, window, cx| {
-                        (self.on_click)(&self.checked.inverse(), window, cx);
                     })
                     .child(self.label),
             )
+            .when(self.checkbox_position == IconPosition::End, |this| {
+                this.child(
+                    Checkbox::new(self.id.clone(), self.checked)
+                        .style(self.style)
+                        .when(self.filled, Checkbox::fill)
+                        .on_click(move |checked, window, cx| {
+                            (self.on_click)(checked, window, cx);
+                        }),
+                )
+            })
     }
 }
 

crates/workspace/src/workspace.rs 🔗

@@ -129,24 +129,6 @@ static ZED_WINDOW_POSITION: LazyLock<Option<Point<Pixels>>> = LazyLock::new(|| {
 
 actions!(assistant, [ShowConfiguration]);
 
-actions!(
-    debugger,
-    [
-        Start,
-        Continue,
-        Disconnect,
-        Pause,
-        Restart,
-        StepInto,
-        StepOver,
-        StepOut,
-        StepBack,
-        Stop,
-        ToggleIgnoreBreakpoints,
-        ClearAllBreakpoints
-    ]
-);
-
 actions!(
     workspace,
     [