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, _, cx| {
421 let context_menu = cx.weak_entity();
422 for session in sessions.into_iter() {
423 let weak_session = session.downgrade();
424 let weak_session_id = weak_session.entity_id();
425
426 this = this.custom_entry(
427 {
428 let weak = weak.clone();
429 let context_menu = context_menu.clone();
430 move |_, cx| {
431 weak_session
432 .read_with(cx, |session, cx| {
433 let context_menu = context_menu.clone();
434 let id: SharedString =
435 format!("debug-session-{}", session.session_id(cx).0)
436 .into();
437 h_flex()
438 .w_full()
439 .group(id.clone())
440 .justify_between()
441 .child(session.label_element(cx))
442 .child(
443 IconButton::new(
444 "close-debug-session",
445 IconName::Close,
446 )
447 .visible_on_hover(id.clone())
448 .icon_size(IconSize::Small)
449 .on_click({
450 let weak = weak.clone();
451 move |_, window, cx| {
452 weak.update(cx, |panel, cx| {
453 panel
454 .close_session(weak_session_id, cx);
455 })
456 .ok();
457 context_menu
458 .update(cx, |this, cx| {
459 this.cancel(
460 &Default::default(),
461 window,
462 cx,
463 );
464 })
465 .ok();
466 }
467 }),
468 )
469 .into_any_element()
470 })
471 .unwrap_or_else(|_| div().into_any_element())
472 }
473 },
474 {
475 let weak = weak.clone();
476 move |window, cx| {
477 weak.update(cx, |panel, cx| {
478 panel.activate_session(session.clone(), window, cx);
479 })
480 .ok();
481 }
482 },
483 );
484 }
485 this
486 }),
487 )
488 }
489
490 fn top_controls_strip(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
491 let active_session = self.active_session.clone();
492
493 Some(
494 h_flex()
495 .border_b_1()
496 .border_color(cx.theme().colors().border)
497 .p_1()
498 .justify_between()
499 .w_full()
500 .child(
501 h_flex().gap_2().w_full().when_some(
502 active_session
503 .as_ref()
504 .and_then(|session| session.read(cx).mode().as_running()),
505 |this, running_session| {
506 let thread_status = running_session
507 .read(cx)
508 .thread_status(cx)
509 .unwrap_or(project::debugger::session::ThreadStatus::Exited);
510 let capabilities = running_session.read(cx).capabilities(cx);
511 this.map(|this| {
512 if thread_status == ThreadStatus::Running {
513 this.child(
514 IconButton::new("debug-pause", IconName::DebugPause)
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.pause_thread(cx);
521 },
522 ))
523 .tooltip(move |window, cx| {
524 Tooltip::text("Pause program")(window, cx)
525 }),
526 )
527 } else {
528 this.child(
529 IconButton::new("debug-continue", IconName::DebugContinue)
530 .icon_size(IconSize::XSmall)
531 .shape(ui::IconButtonShape::Square)
532 .on_click(window.listener_for(
533 &running_session,
534 |this, _, _window, cx| this.continue_thread(cx),
535 ))
536 .disabled(thread_status != ThreadStatus::Stopped)
537 .tooltip(move |window, cx| {
538 Tooltip::text("Continue program")(window, cx)
539 }),
540 )
541 }
542 })
543 .child(
544 IconButton::new("debug-step-over", IconName::ArrowRight)
545 .icon_size(IconSize::XSmall)
546 .shape(ui::IconButtonShape::Square)
547 .on_click(window.listener_for(
548 &running_session,
549 |this, _, _window, cx| {
550 this.step_over(cx);
551 },
552 ))
553 .disabled(thread_status != ThreadStatus::Stopped)
554 .tooltip(move |window, cx| {
555 Tooltip::text("Step over")(window, cx)
556 }),
557 )
558 .child(
559 IconButton::new("debug-step-out", IconName::ArrowUpRight)
560 .icon_size(IconSize::XSmall)
561 .shape(ui::IconButtonShape::Square)
562 .on_click(window.listener_for(
563 &running_session,
564 |this, _, _window, cx| {
565 this.step_out(cx);
566 },
567 ))
568 .disabled(thread_status != ThreadStatus::Stopped)
569 .tooltip(move |window, cx| {
570 Tooltip::text("Step out")(window, cx)
571 }),
572 )
573 .child(
574 IconButton::new("debug-step-into", IconName::ArrowDownRight)
575 .icon_size(IconSize::XSmall)
576 .shape(ui::IconButtonShape::Square)
577 .on_click(window.listener_for(
578 &running_session,
579 |this, _, _window, cx| {
580 this.step_in(cx);
581 },
582 ))
583 .disabled(thread_status != ThreadStatus::Stopped)
584 .tooltip(move |window, cx| {
585 Tooltip::text("Step in")(window, cx)
586 }),
587 )
588 .child(Divider::vertical())
589 .child(
590 IconButton::new(
591 "debug-enable-breakpoint",
592 IconName::DebugDisabledBreakpoint,
593 )
594 .icon_size(IconSize::XSmall)
595 .shape(ui::IconButtonShape::Square)
596 .disabled(thread_status != ThreadStatus::Stopped),
597 )
598 .child(
599 IconButton::new("debug-disable-breakpoint", IconName::CircleOff)
600 .icon_size(IconSize::XSmall)
601 .shape(ui::IconButtonShape::Square)
602 .disabled(thread_status != ThreadStatus::Stopped),
603 )
604 .child(
605 IconButton::new("debug-disable-all-breakpoints", IconName::BugOff)
606 .icon_size(IconSize::XSmall)
607 .shape(ui::IconButtonShape::Square)
608 .disabled(
609 thread_status == ThreadStatus::Exited
610 || thread_status == ThreadStatus::Ended,
611 )
612 .on_click(window.listener_for(
613 &running_session,
614 |this, _, _window, cx| {
615 this.toggle_ignore_breakpoints(cx);
616 },
617 ))
618 .tooltip(move |window, cx| {
619 Tooltip::text("Disable all breakpoints")(window, cx)
620 }),
621 )
622 .child(Divider::vertical())
623 .child(
624 IconButton::new("debug-restart", IconName::DebugRestart)
625 .icon_size(IconSize::XSmall)
626 .on_click(window.listener_for(
627 &running_session,
628 |this, _, _window, cx| {
629 this.restart_session(cx);
630 },
631 ))
632 .disabled(
633 !capabilities.supports_restart_request.unwrap_or_default(),
634 )
635 .tooltip(move |window, cx| {
636 Tooltip::text("Restart")(window, cx)
637 }),
638 )
639 .child(
640 IconButton::new("debug-stop", IconName::Power)
641 .icon_size(IconSize::XSmall)
642 .on_click(window.listener_for(
643 &running_session,
644 |this, _, _window, cx| {
645 this.stop_thread(cx);
646 },
647 ))
648 .disabled(
649 thread_status != ThreadStatus::Stopped
650 && thread_status != ThreadStatus::Running,
651 )
652 .tooltip({
653 let label = if capabilities
654 .supports_terminate_threads_request
655 .unwrap_or_default()
656 {
657 "Terminate Thread"
658 } else {
659 "Terminate all Threads"
660 };
661 move |window, cx| Tooltip::text(label)(window, cx)
662 }),
663 )
664 },
665 ),
666 )
667 .child(
668 h_flex()
669 .gap_2()
670 .when_some(
671 active_session
672 .as_ref()
673 .and_then(|session| session.read(cx).mode().as_running())
674 .cloned(),
675 |this, session| {
676 this.child(
677 session.update(cx, |this, cx| this.thread_dropdown(window, cx)),
678 )
679 .child(Divider::vertical())
680 },
681 )
682 .when_some(active_session.as_ref(), |this, session| {
683 let context_menu = self.sessions_drop_down_menu(session, window, cx);
684 this.child(context_menu).child(Divider::vertical())
685 })
686 .child(
687 IconButton::new("debug-new-session", IconName::Plus)
688 .icon_size(IconSize::Small)
689 .on_click({
690 let workspace = self.workspace.clone();
691 let weak_panel = cx.weak_entity();
692 let past_debug_definition = self.past_debug_definition.clone();
693 move |_, window, cx| {
694 let weak_panel = weak_panel.clone();
695 let past_debug_definition = past_debug_definition.clone();
696
697 let _ = workspace.update(cx, |this, cx| {
698 let workspace = cx.weak_entity();
699 this.toggle_modal(window, cx, |window, cx| {
700 NewSessionModal::new(
701 past_debug_definition,
702 weak_panel,
703 workspace,
704 window,
705 cx,
706 )
707 });
708 });
709 }
710 })
711 .tooltip(|window, cx| {
712 Tooltip::for_action(
713 "New Debug Session",
714 &CreateDebuggingSession,
715 window,
716 cx,
717 )
718 }),
719 ),
720 ),
721 )
722 }
723
724 fn activate_session(
725 &mut self,
726 session_item: Entity<DebugSession>,
727 window: &mut Window,
728 cx: &mut Context<Self>,
729 ) {
730 debug_assert!(self.sessions.contains(&session_item));
731 session_item.focus_handle(cx).focus(window);
732 session_item.update(cx, |this, cx| {
733 if let Some(running) = this.mode().as_running() {
734 running.update(cx, |this, cx| {
735 this.go_to_selected_stack_frame(window, cx);
736 });
737 }
738 });
739 self.active_session = Some(session_item);
740 cx.notify();
741 }
742}
743
744impl EventEmitter<PanelEvent> for DebugPanel {}
745impl EventEmitter<DebugPanelEvent> for DebugPanel {}
746impl EventEmitter<project::Event> for DebugPanel {}
747
748impl Focusable for DebugPanel {
749 fn focus_handle(&self, _: &App) -> FocusHandle {
750 self.focus_handle.clone()
751 }
752}
753
754impl Panel for DebugPanel {
755 fn persistent_name() -> &'static str {
756 "DebugPanel"
757 }
758
759 fn position(&self, _window: &Window, _cx: &App) -> DockPosition {
760 DockPosition::Bottom
761 }
762
763 fn position_is_valid(&self, position: DockPosition) -> bool {
764 position == DockPosition::Bottom
765 }
766
767 fn set_position(
768 &mut self,
769 _position: DockPosition,
770 _window: &mut Window,
771 _cx: &mut Context<Self>,
772 ) {
773 }
774
775 fn size(&self, _window: &Window, _: &App) -> Pixels {
776 self.size
777 }
778
779 fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
780 self.size = size.unwrap();
781 }
782
783 fn remote_id() -> Option<proto::PanelId> {
784 Some(proto::PanelId::DebugPanel)
785 }
786
787 fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
788 Some(IconName::Debug)
789 }
790
791 fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
792 if DebuggerSettings::get_global(cx).button {
793 Some("Debug Panel")
794 } else {
795 None
796 }
797 }
798
799 fn toggle_action(&self) -> Box<dyn Action> {
800 Box::new(ToggleFocus)
801 }
802
803 fn activation_priority(&self) -> u32 {
804 9
805 }
806 fn set_active(&mut self, _: bool, _: &mut Window, _: &mut Context<Self>) {}
807}
808
809impl Render for DebugPanel {
810 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
811 let has_sessions = self.sessions.len() > 0;
812 debug_assert_eq!(has_sessions, self.active_session.is_some());
813
814 v_flex()
815 .size_full()
816 .key_context("DebugPanel")
817 .child(h_flex().children(self.top_controls_strip(window, cx)))
818 .track_focus(&self.focus_handle(cx))
819 .map(|this| {
820 if has_sessions {
821 this.children(self.active_session.clone())
822 } else {
823 this.child(
824 v_flex()
825 .h_full()
826 .gap_1()
827 .items_center()
828 .justify_center()
829 .child(
830 h_flex().child(
831 Label::new("No Debugging Sessions")
832 .size(LabelSize::Small)
833 .color(Color::Muted),
834 ),
835 )
836 .child(
837 h_flex().flex_shrink().child(
838 Button::new("spawn-new-session-empty-state", "New Session")
839 .size(ButtonSize::Large)
840 .on_click(|_, window, cx| {
841 window.dispatch_action(
842 CreateDebuggingSession.boxed_clone(),
843 cx,
844 );
845 }),
846 ),
847 ),
848 )
849 }
850 })
851 .into_any()
852 }
853}