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