debugger_panel.rs

  1use crate::{
  2    ClearAllBreakpoints, Continue, CreateDebuggingSession, Disconnect, Pause, Restart, StepBack,
  3    StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
  4};
  5use crate::{new_session_modal::NewSessionModal, session::DebugSession};
  6use anyhow::{Result, anyhow};
  7use collections::HashMap;
  8use command_palette_hooks::CommandPaletteFilter;
  9use dap::{
 10    ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
 11    client::SessionId, debugger_settings::DebuggerSettings,
 12};
 13use futures::{SinkExt as _, channel::mpsc};
 14use gpui::{
 15    Action, App, AsyncWindowContext, Context, Entity, EntityId, EventEmitter, FocusHandle,
 16    Focusable, Subscription, Task, WeakEntity, actions,
 17};
 18use project::{
 19    Project,
 20    debugger::{
 21        dap_store::{self, DapStore},
 22        session::ThreadStatus,
 23    },
 24    terminals::TerminalKind,
 25};
 26use rpc::proto::{self};
 27use settings::Settings;
 28use std::any::TypeId;
 29use std::path::Path;
 30use std::sync::Arc;
 31use task::DebugTaskDefinition;
 32use terminal_view::terminal_panel::TerminalPanel;
 33use ui::{ContextMenu, Divider, DropdownMenu, Tooltip, prelude::*};
 34use workspace::{
 35    Workspace,
 36    dock::{DockPosition, Panel, PanelEvent},
 37};
 38
 39pub enum DebugPanelEvent {
 40    Exited(SessionId),
 41    Terminated(SessionId),
 42    Stopped {
 43        client_id: SessionId,
 44        event: StoppedEvent,
 45        go_to_stack_frame: bool,
 46    },
 47    Thread((SessionId, ThreadEvent)),
 48    Continued((SessionId, ContinuedEvent)),
 49    Output((SessionId, OutputEvent)),
 50    Module((SessionId, ModuleEvent)),
 51    LoadedSource((SessionId, LoadedSourceEvent)),
 52    ClientShutdown(SessionId),
 53    CapabilitiesChanged(SessionId),
 54}
 55
 56actions!(debug_panel, [ToggleFocus]);
 57pub struct DebugPanel {
 58    size: Pixels,
 59    sessions: Vec<Entity<DebugSession>>,
 60    active_session: Option<Entity<DebugSession>>,
 61    /// This represents the last debug definition that was created in the new session modal
 62    pub(crate) past_debug_definition: Option<DebugTaskDefinition>,
 63    project: WeakEntity<Project>,
 64    workspace: WeakEntity<Workspace>,
 65    focus_handle: FocusHandle,
 66    _subscriptions: Vec<Subscription>,
 67}
 68
 69impl DebugPanel {
 70    pub fn new(
 71        workspace: &Workspace,
 72        window: &mut Window,
 73        cx: &mut Context<Workspace>,
 74    ) -> Entity<Self> {
 75        cx.new(|cx| {
 76            let project = workspace.project().clone();
 77            let dap_store = project.read(cx).dap_store();
 78
 79            let _subscriptions =
 80                vec![cx.subscribe_in(&dap_store, window, Self::handle_dap_store_event)];
 81
 82            let debug_panel = Self {
 83                size: px(300.),
 84                sessions: vec![],
 85                active_session: None,
 86                _subscriptions,
 87                past_debug_definition: None,
 88                focus_handle: cx.focus_handle(),
 89                project: project.downgrade(),
 90                workspace: workspace.weak_handle(),
 91            };
 92
 93            debug_panel
 94        })
 95    }
 96
 97    pub fn load(
 98        workspace: WeakEntity<Workspace>,
 99        cx: AsyncWindowContext,
100    ) -> Task<Result<Entity<Self>>> {
101        cx.spawn(async move |cx| {
102            workspace.update_in(cx, |workspace, window, cx| {
103                let debug_panel = DebugPanel::new(workspace, window, cx);
104
105                workspace.register_action(|workspace, _: &ClearAllBreakpoints, _, cx| {
106                    workspace.project().read(cx).breakpoint_store().update(
107                        cx,
108                        |breakpoint_store, cx| {
109                            breakpoint_store.clear_breakpoints(cx);
110                        },
111                    )
112                });
113
114                cx.observe(&debug_panel, |_, debug_panel, cx| {
115                    let (has_active_session, supports_restart, support_step_back) = debug_panel
116                        .update(cx, |this, cx| {
117                            this.active_session()
118                                .map(|item| {
119                                    let running = item.read(cx).mode().as_running().cloned();
120
121                                    match running {
122                                        Some(running) => {
123                                            let caps = running.read(cx).capabilities(cx);
124                                            (
125                                                true,
126                                                caps.supports_restart_request.unwrap_or_default(),
127                                                caps.supports_step_back.unwrap_or_default(),
128                                            )
129                                        }
130                                        None => (false, false, false),
131                                    }
132                                })
133                                .unwrap_or((false, false, false))
134                        });
135
136                    let filter = CommandPaletteFilter::global_mut(cx);
137                    let debugger_action_types = [
138                        TypeId::of::<Continue>(),
139                        TypeId::of::<StepOver>(),
140                        TypeId::of::<StepInto>(),
141                        TypeId::of::<StepOut>(),
142                        TypeId::of::<Stop>(),
143                        TypeId::of::<Disconnect>(),
144                        TypeId::of::<Pause>(),
145                        TypeId::of::<ToggleIgnoreBreakpoints>(),
146                    ];
147
148                    let step_back_action_type = [TypeId::of::<StepBack>()];
149                    let restart_action_type = [TypeId::of::<Restart>()];
150
151                    if has_active_session {
152                        filter.show_action_types(debugger_action_types.iter());
153
154                        if supports_restart {
155                            filter.show_action_types(restart_action_type.iter());
156                        } else {
157                            filter.hide_action_types(&restart_action_type);
158                        }
159
160                        if support_step_back {
161                            filter.show_action_types(step_back_action_type.iter());
162                        } else {
163                            filter.hide_action_types(&step_back_action_type);
164                        }
165                    } else {
166                        // show only the `debug: start`
167                        filter.hide_action_types(&debugger_action_types);
168                        filter.hide_action_types(&step_back_action_type);
169                        filter.hide_action_types(&restart_action_type);
170                    }
171                })
172                .detach();
173
174                debug_panel
175            })
176        })
177    }
178
179    pub fn active_session(&self) -> Option<Entity<DebugSession>> {
180        self.active_session.clone()
181    }
182
183    pub fn debug_panel_items_by_client(
184        &self,
185        client_id: &SessionId,
186        cx: &Context<Self>,
187    ) -> Vec<Entity<DebugSession>> {
188        self.sessions
189            .iter()
190            .filter(|item| item.read(cx).session_id(cx) == *client_id)
191            .map(|item| item.clone())
192            .collect()
193    }
194
195    pub fn debug_panel_item_by_client(
196        &self,
197        client_id: SessionId,
198        cx: &mut Context<Self>,
199    ) -> Option<Entity<DebugSession>> {
200        self.sessions
201            .iter()
202            .find(|item| {
203                let item = item.read(cx);
204
205                item.session_id(cx) == client_id
206            })
207            .cloned()
208    }
209
210    fn handle_dap_store_event(
211        &mut self,
212        dap_store: &Entity<DapStore>,
213        event: &dap_store::DapStoreEvent,
214        window: &mut Window,
215        cx: &mut Context<Self>,
216    ) {
217        match event {
218            dap_store::DapStoreEvent::DebugSessionInitialized(session_id) => {
219                let Some(session) = dap_store.read(cx).session_by_id(session_id) else {
220                    return log::error!(
221                        "Couldn't get session with id: {session_id:?} from DebugClientStarted event"
222                    );
223                };
224
225                let Some(project) = self.project.upgrade() else {
226                    return log::error!("Debug Panel out lived it's weak reference to Project");
227                };
228
229                if self
230                    .sessions
231                    .iter()
232                    .any(|item| item.read(cx).session_id(cx) == *session_id)
233                {
234                    // We already have an item for this session.
235                    return;
236                }
237                let session_item = DebugSession::running(
238                    project,
239                    self.workspace.clone(),
240                    session,
241                    cx.weak_entity(),
242                    window,
243                    cx,
244                );
245
246                self.sessions.push(session_item.clone());
247                self.activate_session(session_item, window, cx);
248            }
249            dap_store::DapStoreEvent::RunInTerminal {
250                title,
251                cwd,
252                command,
253                args,
254                envs,
255                sender,
256                ..
257            } => {
258                self.handle_run_in_terminal_request(
259                    title.clone(),
260                    cwd.clone(),
261                    command.clone(),
262                    args.clone(),
263                    envs.clone(),
264                    sender.clone(),
265                    window,
266                    cx,
267                )
268                .detach_and_log_err(cx);
269            }
270            _ => {}
271        }
272    }
273
274    fn handle_run_in_terminal_request(
275        &self,
276        title: Option<String>,
277        cwd: Option<Arc<Path>>,
278        command: Option<String>,
279        args: Vec<String>,
280        envs: HashMap<String, String>,
281        mut sender: mpsc::Sender<Result<u32>>,
282        window: &mut Window,
283        cx: &mut App,
284    ) -> Task<Result<()>> {
285        let terminal_task = self.workspace.update(cx, |workspace, cx| {
286            let terminal_panel = workspace.panel::<TerminalPanel>(cx).ok_or_else(|| {
287                anyhow!("RunInTerminal DAP request failed because TerminalPanel wasn't found")
288            });
289
290            let terminal_panel = match terminal_panel {
291                Ok(panel) => panel,
292                Err(err) => return Task::ready(Err(err)),
293            };
294
295            terminal_panel.update(cx, |terminal_panel, cx| {
296                let terminal_task = terminal_panel.add_terminal(
297                    TerminalKind::Debug {
298                        command,
299                        args,
300                        envs,
301                        cwd,
302                        title,
303                    },
304                    task::RevealStrategy::Always,
305                    window,
306                    cx,
307                );
308
309                cx.spawn(async move |_, cx| {
310                    let pid_task = async move {
311                        let terminal = terminal_task.await?;
312
313                        terminal.read_with(cx, |terminal, _| terminal.pty_info.pid())
314                    };
315
316                    pid_task.await
317                })
318            })
319        });
320
321        cx.background_spawn(async move {
322            match terminal_task {
323                Ok(pid_task) => match pid_task.await {
324                    Ok(Some(pid)) => sender.send(Ok(pid.as_u32())).await?,
325                    Ok(None) => {
326                        sender
327                            .send(Err(anyhow!(
328                                "Terminal was spawned but PID was not available"
329                            )))
330                            .await?
331                    }
332                    Err(error) => sender.send(Err(anyhow!(error))).await?,
333                },
334                Err(error) => sender.send(Err(anyhow!(error))).await?,
335            };
336
337            Ok(())
338        })
339    }
340
341    fn close_session(&mut self, entity_id: EntityId, cx: &mut Context<Self>) {
342        let Some(session) = self
343            .sessions
344            .iter()
345            .find(|other| entity_id == other.entity_id())
346        else {
347            return;
348        };
349
350        session.update(cx, |session, cx| session.shutdown(cx));
351
352        self.sessions.retain(|other| entity_id != other.entity_id());
353
354        if let Some(active_session_id) = self
355            .active_session
356            .as_ref()
357            .map(|session| session.entity_id())
358        {
359            if active_session_id == entity_id {
360                self.active_session = self.sessions.first().cloned();
361            }
362        }
363    }
364
365    fn sessions_drop_down_menu(
366        &self,
367        active_session: &Entity<DebugSession>,
368        window: &mut Window,
369        cx: &mut Context<Self>,
370    ) -> DropdownMenu {
371        let sessions = self.sessions.clone();
372        let weak = cx.weak_entity();
373        let label = active_session.read(cx).label_element(cx);
374
375        DropdownMenu::new_with_element(
376            "debugger-session-list",
377            label,
378            ContextMenu::build(window, cx, move |mut this, _, _| {
379                for session in sessions.into_iter() {
380                    let weak_session = session.downgrade();
381                    let weak_id = weak_session.entity_id();
382
383                    this = this.custom_entry(
384                        {
385                            let weak = weak.clone();
386                            move |_, cx| {
387                                weak_session
388                                    .read_with(cx, |session, cx| {
389                                        h_flex()
390                                            .w_full()
391                                            .justify_between()
392                                            .child(session.label_element(cx))
393                                            .child(
394                                                IconButton::new(
395                                                    "close-debug-session",
396                                                    IconName::Close,
397                                                )
398                                                .icon_size(IconSize::Small)
399                                                .on_click({
400                                                    let weak = weak.clone();
401                                                    move |_, _, cx| {
402                                                        weak.update(cx, |panel, cx| {
403                                                            panel.close_session(weak_id, cx);
404                                                        })
405                                                        .ok();
406                                                    }
407                                                }),
408                                            )
409                                            .into_any_element()
410                                    })
411                                    .unwrap_or_else(|_| div().into_any_element())
412                            }
413                        },
414                        {
415                            let weak = weak.clone();
416                            move |window, cx| {
417                                weak.update(cx, |panel, cx| {
418                                    panel.activate_session(session.clone(), window, cx);
419                                })
420                                .ok();
421                            }
422                        },
423                    );
424                }
425                this
426            }),
427        )
428    }
429
430    fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
431        let active_session = self.active_session.clone();
432
433        Some(
434            h_flex()
435                .border_b_1()
436                .border_color(cx.theme().colors().border)
437                .p_1()
438                .justify_between()
439                .w_full()
440                .child(
441                    h_flex().gap_2().w_full().when_some(
442                        active_session
443                            .as_ref()
444                            .and_then(|session| session.read(cx).mode().as_running()),
445                        |this, running_session| {
446                            let thread_status = running_session
447                                .read(cx)
448                                .thread_status(cx)
449                                .unwrap_or(project::debugger::session::ThreadStatus::Exited);
450                            let capabilities = running_session.read(cx).capabilities(cx);
451                            this.map(|this| {
452                                if thread_status == ThreadStatus::Running {
453                                    this.child(
454                                        IconButton::new("debug-pause", IconName::DebugPause)
455                                            .icon_size(IconSize::XSmall)
456                                            .shape(ui::IconButtonShape::Square)
457                                            .on_click(window.listener_for(
458                                                &running_session,
459                                                |this, _, _window, cx| {
460                                                    this.pause_thread(cx);
461                                                },
462                                            ))
463                                            .tooltip(move |window, cx| {
464                                                Tooltip::text("Pause program")(window, cx)
465                                            }),
466                                    )
467                                } else {
468                                    this.child(
469                                        IconButton::new("debug-continue", IconName::DebugContinue)
470                                            .icon_size(IconSize::XSmall)
471                                            .shape(ui::IconButtonShape::Square)
472                                            .on_click(window.listener_for(
473                                                &running_session,
474                                                |this, _, _window, cx| this.continue_thread(cx),
475                                            ))
476                                            .disabled(thread_status != ThreadStatus::Stopped)
477                                            .tooltip(move |window, cx| {
478                                                Tooltip::text("Continue program")(window, cx)
479                                            }),
480                                    )
481                                }
482                            })
483                            .child(
484                                IconButton::new("debug-step-over", IconName::ArrowRight)
485                                    .icon_size(IconSize::XSmall)
486                                    .shape(ui::IconButtonShape::Square)
487                                    .on_click(window.listener_for(
488                                        &running_session,
489                                        |this, _, _window, cx| {
490                                            this.step_over(cx);
491                                        },
492                                    ))
493                                    .disabled(thread_status != ThreadStatus::Stopped)
494                                    .tooltip(move |window, cx| {
495                                        Tooltip::text("Step over")(window, cx)
496                                    }),
497                            )
498                            .child(
499                                IconButton::new("debug-step-out", IconName::ArrowUpRight)
500                                    .icon_size(IconSize::XSmall)
501                                    .shape(ui::IconButtonShape::Square)
502                                    .on_click(window.listener_for(
503                                        &running_session,
504                                        |this, _, _window, cx| {
505                                            this.step_out(cx);
506                                        },
507                                    ))
508                                    .disabled(thread_status != ThreadStatus::Stopped)
509                                    .tooltip(move |window, cx| {
510                                        Tooltip::text("Step out")(window, cx)
511                                    }),
512                            )
513                            .child(
514                                IconButton::new("debug-step-into", IconName::ArrowDownRight)
515                                    .icon_size(IconSize::XSmall)
516                                    .shape(ui::IconButtonShape::Square)
517                                    .on_click(window.listener_for(
518                                        &running_session,
519                                        |this, _, _window, cx| {
520                                            this.step_in(cx);
521                                        },
522                                    ))
523                                    .disabled(thread_status != ThreadStatus::Stopped)
524                                    .tooltip(move |window, cx| {
525                                        Tooltip::text("Step in")(window, cx)
526                                    }),
527                            )
528                            .child(Divider::vertical())
529                            .child(
530                                IconButton::new(
531                                    "debug-enable-breakpoint",
532                                    IconName::DebugDisabledBreakpoint,
533                                )
534                                .icon_size(IconSize::XSmall)
535                                .shape(ui::IconButtonShape::Square)
536                                .disabled(thread_status != ThreadStatus::Stopped),
537                            )
538                            .child(
539                                IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
540                                    .icon_size(IconSize::XSmall)
541                                    .shape(ui::IconButtonShape::Square)
542                                    .disabled(thread_status != ThreadStatus::Stopped),
543                            )
544                            .child(
545                                IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
546                                    .icon_size(IconSize::XSmall)
547                                    .shape(ui::IconButtonShape::Square)
548                                    .disabled(
549                                        thread_status == ThreadStatus::Exited
550                                            || thread_status == ThreadStatus::Ended,
551                                    )
552                                    .on_click(window.listener_for(
553                                        &running_session,
554                                        |this, _, _window, cx| {
555                                            this.toggle_ignore_breakpoints(cx);
556                                        },
557                                    ))
558                                    .tooltip(move |window, cx| {
559                                        Tooltip::text("Disable all breakpoints")(window, cx)
560                                    }),
561                            )
562                            .child(Divider::vertical())
563                            .child(
564                                IconButton::new("debug-restart", IconName::DebugRestart)
565                                    .icon_size(IconSize::XSmall)
566                                    .on_click(window.listener_for(
567                                        &running_session,
568                                        |this, _, _window, cx| {
569                                            this.restart_session(cx);
570                                        },
571                                    ))
572                                    .disabled(
573                                        !capabilities.supports_restart_request.unwrap_or_default(),
574                                    )
575                                    .tooltip(move |window, cx| {
576                                        Tooltip::text("Restart")(window, cx)
577                                    }),
578                            )
579                            .child(
580                                IconButton::new("debug-stop", IconName::Power)
581                                    .icon_size(IconSize::XSmall)
582                                    .on_click(window.listener_for(
583                                        &running_session,
584                                        |this, _, _window, cx| {
585                                            this.stop_thread(cx);
586                                        },
587                                    ))
588                                    .disabled(
589                                        thread_status != ThreadStatus::Stopped
590                                            && thread_status != ThreadStatus::Running,
591                                    )
592                                    .tooltip({
593                                        let label = if capabilities
594                                            .supports_terminate_threads_request
595                                            .unwrap_or_default()
596                                        {
597                                            "Terminate Thread"
598                                        } else {
599                                            "Terminate all Threads"
600                                        };
601                                        move |window, cx| Tooltip::text(label)(window, cx)
602                                    }),
603                            )
604                        },
605                    ),
606                )
607                .child(
608                    h_flex()
609                        .gap_2()
610                        .when_some(
611                            active_session
612                                .as_ref()
613                                .and_then(|session| session.read(cx).mode().as_running())
614                                .cloned(),
615                            |this, session| {
616                                this.child(
617                                    session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
618                                )
619                                .child(Divider::vertical())
620                            },
621                        )
622                        .when_some(active_session.as_ref(), |this, session| {
623                            let context_menu = self.sessions_drop_down_menu(session, window, cx);
624                            this.child(context_menu).child(Divider::vertical())
625                        })
626                        .child(
627                            IconButton::new("debug-new-session", IconName::Plus)
628                                .icon_size(IconSize::Small)
629                                .on_click({
630                                    let workspace = self.workspace.clone();
631                                    let weak_panel = cx.weak_entity();
632                                    let past_debug_definition = self.past_debug_definition.clone();
633                                    move |_, window, cx| {
634                                        let weak_panel = weak_panel.clone();
635                                        let past_debug_definition = past_debug_definition.clone();
636
637                                        let _ = workspace.update(cx, |this, cx| {
638                                            let workspace = cx.weak_entity();
639                                            this.toggle_modal(window, cx, |window, cx| {
640                                                NewSessionModal::new(
641                                                    past_debug_definition,
642                                                    weak_panel,
643                                                    workspace,
644                                                    window,
645                                                    cx,
646                                                )
647                                            });
648                                        });
649                                    }
650                                })
651                                .tooltip(|window, cx| {
652                                    Tooltip::for_action(
653                                        "New Debug Session",
654                                        &CreateDebuggingSession,
655                                        window,
656                                        cx,
657                                    )
658                                }),
659                        ),
660                ),
661        )
662    }
663
664    fn activate_session(
665        &mut self,
666        session_item: Entity<DebugSession>,
667        window: &mut Window,
668        cx: &mut Context<Self>,
669    ) {
670        debug_assert!(self.sessions.contains(&session_item));
671        session_item.focus_handle(cx).focus(window);
672        session_item.update(cx, |this, cx| {
673            if let Some(running) = this.mode().as_running() {
674                running.update(cx, |this, cx| {
675                    this.go_to_selected_stack_frame(window, cx);
676                });
677            }
678        });
679        self.active_session = Some(session_item);
680        cx.notify();
681    }
682}
683
684impl EventEmitter<PanelEvent> for DebugPanel {}
685impl EventEmitter<DebugPanelEvent> for DebugPanel {}
686impl EventEmitter<project::Event> for DebugPanel {}
687
688impl Focusable for DebugPanel {
689    fn focus_handle(&self, _: &App) -> FocusHandle {
690        self.focus_handle.clone()
691    }
692}
693
694impl Panel for DebugPanel {
695    fn persistent_name() -> &'static str {
696        "DebugPanel"
697    }
698
699    fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
700        DockPosition::Bottom
701    }
702
703    fn position_is_valid(&self, position: DockPosition) -> bool {
704        position == DockPosition::Bottom
705    }
706
707    fn set_position(
708        &mut self,
709        _position: DockPosition,
710        _window: &mut Window,
711        _cx: &mut Context<Self>,
712    ) {
713    }
714
715    fn size(&self, _window: &Window, _: &App) -> Pixels {
716        self.size
717    }
718
719    fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
720        self.size = size.unwrap();
721    }
722
723    fn remote_id() -> Option<proto::PanelId> {
724        Some(proto::PanelId::DebugPanel)
725    }
726
727    fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
728        Some(IconName::Debug)
729    }
730
731    fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
732        if DebuggerSettings::get_global(cx).button {
733            Some("Debug Panel")
734        } else {
735            None
736        }
737    }
738
739    fn toggle_action(&self) -> Box<dyn Action> {
740        Box::new(ToggleFocus)
741    }
742
743    fn activation_priority(&self) -> u32 {
744        9
745    }
746    fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
747}
748
749impl Render for DebugPanel {
750    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
751        let has_sessions = self.sessions.len() > 0;
752        debug_assert_eq!(has_sessions, self.active_session.is_some());
753
754        v_flex()
755            .size_full()
756            .key_context("DebugPanel")
757            .child(h_flex().children(self.top_controls_strip(window, cx)))
758            .track_focus(&self.focus_handle(cx))
759            .map(|this| {
760                if has_sessions {
761                    this.children(self.active_session.clone())
762                } else {
763                    this.child(
764                        v_flex()
765                            .h_full()
766                            .gap_1()
767                            .items_center()
768                            .justify_center()
769                            .child(
770                                h_flex().child(
771                                    Label::new("No Debugging Sessions")
772                                        .size(LabelSize::Small)
773                                        .color(Color::Muted),
774                                ),
775                            )
776                            .child(
777                                h_flex().flex_shrink().child(
778                                    Button::new("spawn-new-session-empty-state", "New Session")
779                                        .size(ButtonSize::Large)
780                                        .on_click(|_, window, cx| {
781                                            window.dispatch_action(
782                                                CreateDebuggingSession.boxed_clone(),
783                                                cx,
784                                            );
785                                        }),
786                                ),
787                            ),
788                    )
789                }
790            })
791            .into_any()
792    }
793}