1pub(crate) mod breakpoint_list;
2pub(crate) mod console;
3pub(crate) mod loaded_source_list;
4pub(crate) mod module_list;
5pub mod stack_frame_list;
6pub mod variable_list;
7
8use std::{any::Any, ops::ControlFlow, sync::Arc, time::Duration};
9
10use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
11
12use super::DebugPanelItemEvent;
13use breakpoint_list::BreakpointList;
14use collections::{HashMap, IndexMap};
15use console::Console;
16use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
17use gpui::{
18 Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
19 NoAction, Pixels, Point, Subscription, Task, WeakEntity,
20};
21use loaded_source_list::LoadedSourceList;
22use module_list::ModuleList;
23use project::{
24 Project,
25 debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
26};
27use rpc::proto::ViewId;
28use settings::Settings;
29use stack_frame_list::StackFrameList;
30use terminal_view::TerminalView;
31use ui::{
32 ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
33 DropdownMenu, FluentBuilder, IconButton, IconName, IconSize, InteractiveElement, IntoElement,
34 Label, LabelCommon as _, ParentElement, Render, SharedString, StatefulInteractiveElement,
35 Styled, Tab, Tooltip, VisibleOnHover, Window, div, h_flex, v_flex,
36};
37use util::ResultExt;
38use variable_list::VariableList;
39use workspace::{
40 ActivePaneDecorator, DraggedTab, Item, ItemHandle, Member, Pane, PaneGroup, SplitDirection,
41 Workspace, item::TabContentParams, move_item, pane::Event,
42};
43
44pub struct RunningState {
45 session: Entity<Session>,
46 thread_id: Option<ThreadId>,
47 focus_handle: FocusHandle,
48 _remote_id: Option<ViewId>,
49 workspace: WeakEntity<Workspace>,
50 session_id: SessionId,
51 variable_list: Entity<variable_list::VariableList>,
52 _subscriptions: Vec<Subscription>,
53 stack_frame_list: Entity<stack_frame_list::StackFrameList>,
54 loaded_sources_list: Entity<LoadedSourceList>,
55 pub debug_terminal: Entity<DebugTerminal>,
56 module_list: Entity<module_list::ModuleList>,
57 console: Entity<Console>,
58 breakpoint_list: Entity<BreakpointList>,
59 panes: PaneGroup,
60 active_pane: Option<Entity<Pane>>,
61 pane_close_subscriptions: HashMap<EntityId, Subscription>,
62 _schedule_serialize: Option<Task<()>>,
63}
64
65impl Render for RunningState {
66 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
67 let zoomed_pane = self
68 .panes
69 .panes()
70 .into_iter()
71 .find(|pane| pane.read(cx).is_zoomed());
72
73 let active = self.panes.panes().into_iter().next();
74 let x = if let Some(ref zoomed_pane) = zoomed_pane {
75 zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
76 } else if let Some(active) = active {
77 self.panes
78 .render(
79 None,
80 &ActivePaneDecorator::new(active, &self.workspace),
81 window,
82 cx,
83 )
84 .into_any_element()
85 } else {
86 div().into_any_element()
87 };
88 let thread_status = self
89 .thread_id
90 .map(|thread_id| self.session.read(cx).thread_status(thread_id))
91 .unwrap_or(ThreadStatus::Exited);
92
93 self.variable_list.update(cx, |this, cx| {
94 this.disabled(thread_status != ThreadStatus::Stopped, cx);
95 });
96 v_flex()
97 .size_full()
98 .key_context("DebugSessionItem")
99 .track_focus(&self.focus_handle(cx))
100 .child(h_flex().flex_1().child(x))
101 }
102}
103
104pub(crate) struct SubView {
105 inner: AnyView,
106 pane_focus_handle: FocusHandle,
107 kind: DebuggerPaneItem,
108 show_indicator: Box<dyn Fn(&App) -> bool>,
109 hovered: bool,
110}
111
112impl SubView {
113 pub(crate) fn new(
114 pane_focus_handle: FocusHandle,
115 view: AnyView,
116 kind: DebuggerPaneItem,
117 show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
118 cx: &mut App,
119 ) -> Entity<Self> {
120 cx.new(|_| Self {
121 kind,
122 inner: view,
123 pane_focus_handle,
124 show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
125 hovered: false,
126 })
127 }
128
129 pub(crate) fn view_kind(&self) -> DebuggerPaneItem {
130 self.kind
131 }
132}
133impl Focusable for SubView {
134 fn focus_handle(&self, _: &App) -> FocusHandle {
135 self.pane_focus_handle.clone()
136 }
137}
138impl EventEmitter<()> for SubView {}
139impl Item for SubView {
140 type Event = ();
141
142 /// This is used to serialize debugger pane layouts
143 /// A SharedString gets converted to a enum and back during serialization/deserialization.
144 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
145 self.kind.to_shared_string()
146 }
147
148 fn tab_content(
149 &self,
150 params: workspace::item::TabContentParams,
151 _: &Window,
152 cx: &App,
153 ) -> AnyElement {
154 let label = Label::new(self.kind.to_shared_string())
155 .size(ui::LabelSize::Small)
156 .color(params.text_color())
157 .line_height_style(ui::LineHeightStyle::UiLabel);
158
159 if !params.selected && self.show_indicator.as_ref()(cx) {
160 return h_flex()
161 .justify_between()
162 .child(ui::Indicator::dot())
163 .gap_2()
164 .child(label)
165 .into_any_element();
166 }
167
168 label.into_any_element()
169 }
170}
171
172impl Render for SubView {
173 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
174 v_flex()
175 .id(SharedString::from(format!(
176 "subview-container-{}",
177 self.kind.to_shared_string()
178 )))
179 .on_hover(cx.listener(|this, hovered, _, cx| {
180 this.hovered = *hovered;
181 cx.notify();
182 }))
183 .size_full()
184 // Add border uncoditionally to prevent layout shifts on focus changes.
185 .border_1()
186 .when(self.pane_focus_handle.contains_focused(window, cx), |el| {
187 el.border_color(cx.theme().colors().pane_focused_border)
188 })
189 .child(self.inner.clone())
190 }
191}
192
193pub(crate) fn new_debugger_pane(
194 workspace: WeakEntity<Workspace>,
195 project: Entity<Project>,
196 window: &mut Window,
197 cx: &mut Context<RunningState>,
198) -> Entity<Pane> {
199 let weak_running = cx.weak_entity();
200 let custom_drop_handle = {
201 let workspace = workspace.clone();
202 let project = project.downgrade();
203 let weak_running = weak_running.clone();
204 move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context<Pane>| {
205 let Some(tab) = any.downcast_ref::<DraggedTab>() else {
206 return ControlFlow::Break(());
207 };
208 let Some(project) = project.upgrade() else {
209 return ControlFlow::Break(());
210 };
211 let this_pane = cx.entity().clone();
212 let item = if tab.pane == this_pane {
213 pane.item_for_index(tab.ix)
214 } else {
215 tab.pane.read(cx).item_for_index(tab.ix)
216 };
217 let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
218 return ControlFlow::Break(());
219 };
220
221 let source = tab.pane.clone();
222 let item_id_to_move = item.item_id();
223
224 let Ok(new_split_pane) = pane
225 .drag_split_direction()
226 .map(|split_direction| {
227 weak_running.update(cx, |running, cx| {
228 let new_pane =
229 new_debugger_pane(workspace.clone(), project.clone(), window, cx);
230 let _previous_subscription = running.pane_close_subscriptions.insert(
231 new_pane.entity_id(),
232 cx.subscribe_in(&new_pane, window, RunningState::handle_pane_event),
233 );
234 debug_assert!(_previous_subscription.is_none());
235 running
236 .panes
237 .split(&this_pane, &new_pane, split_direction)?;
238 anyhow::Ok(new_pane)
239 })
240 })
241 .transpose()
242 else {
243 return ControlFlow::Break(());
244 };
245
246 match new_split_pane.transpose() {
247 // Source pane may be the one currently updated, so defer the move.
248 Ok(Some(new_pane)) => cx
249 .spawn_in(window, async move |_, cx| {
250 cx.update(|window, cx| {
251 move_item(
252 &source,
253 &new_pane,
254 item_id_to_move,
255 new_pane.read(cx).active_item_index(),
256 window,
257 cx,
258 );
259 })
260 .ok();
261 })
262 .detach(),
263 // If we drop into existing pane or current pane,
264 // regular pane drop handler will take care of it,
265 // using the right tab index for the operation.
266 Ok(None) => return ControlFlow::Continue(()),
267 err @ Err(_) => {
268 err.log_err();
269 return ControlFlow::Break(());
270 }
271 };
272
273 ControlFlow::Break(())
274 }
275 };
276
277 let ret = cx.new(move |cx| {
278 let mut pane = Pane::new(
279 workspace.clone(),
280 project.clone(),
281 Default::default(),
282 None,
283 NoAction.boxed_clone(),
284 window,
285 cx,
286 );
287 let focus_handle = pane.focus_handle(cx);
288 pane.set_can_split(Some(Arc::new({
289 let weak_running = weak_running.clone();
290 move |pane, dragged_item, _window, cx| {
291 if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
292 let is_current_pane = tab.pane == cx.entity();
293 let Some(can_drag_away) = weak_running
294 .update(cx, |running_state, _| {
295 let current_panes = running_state.panes.panes();
296 !current_panes.contains(&&tab.pane)
297 || current_panes.len() > 1
298 || (!is_current_pane || pane.items_len() > 1)
299 })
300 .ok()
301 else {
302 return false;
303 };
304 if can_drag_away {
305 let item = if is_current_pane {
306 pane.item_for_index(tab.ix)
307 } else {
308 tab.pane.read(cx).item_for_index(tab.ix)
309 };
310 if let Some(item) = item {
311 return item.downcast::<SubView>().is_some();
312 }
313 }
314 }
315 false
316 }
317 })));
318 pane.display_nav_history_buttons(None);
319 pane.set_custom_drop_handle(cx, custom_drop_handle);
320 pane.set_should_display_tab_bar(|_, _| true);
321 pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None));
322 pane.set_render_tab_bar(cx, {
323 move |pane, window, cx| {
324 let active_pane_item = pane.active_item();
325 let pane_group_id: SharedString =
326 format!("pane-zoom-button-hover-{}", cx.entity_id()).into();
327 let is_hovered = active_pane_item.as_ref().map_or(false, |item| {
328 item.downcast::<SubView>()
329 .map_or(false, |this| this.read(cx).hovered)
330 });
331 h_flex()
332 .group(pane_group_id.clone())
333 .justify_between()
334 .bg(cx.theme().colors().tab_bar_background)
335 .border_b_1()
336 .px_2()
337 .border_color(cx.theme().colors().border)
338 .track_focus(&focus_handle)
339 .child(
340 h_flex()
341 .w_full()
342 .gap_1()
343 .h(Tab::container_height(cx))
344 .drag_over::<DraggedTab>(|bar, _, _, cx| {
345 bar.bg(cx.theme().colors().drop_target_background)
346 })
347 .on_drop(cx.listener(
348 move |this, dragged_tab: &DraggedTab, window, cx| {
349 this.drag_split_direction = None;
350 this.handle_tab_drop(dragged_tab, this.items_len(), window, cx)
351 },
352 ))
353 .children(pane.items().enumerate().map(|(ix, item)| {
354 let selected = active_pane_item
355 .as_ref()
356 .map_or(false, |active| active.item_id() == item.item_id());
357 let deemphasized = !pane.has_focus(window, cx);
358 let item_ = item.boxed_clone();
359 div()
360 .id(SharedString::from(format!(
361 "debugger_tab_{}",
362 item.item_id().as_u64()
363 )))
364 .p_1()
365 .rounded_md()
366 .cursor_pointer()
367 .map(|this| {
368 let theme = cx.theme();
369 if selected {
370 let color = theme.colors().tab_active_background;
371 let color = if deemphasized {
372 color.opacity(0.5)
373 } else {
374 color
375 };
376 this.bg(color)
377 } else {
378 let hover_color = theme.colors().element_hover;
379 this.hover(|style| style.bg(hover_color))
380 }
381 })
382 .on_click(cx.listener(move |this, _, window, cx| {
383 let index = this.index_for_item(&*item_);
384 if let Some(index) = index {
385 this.activate_item(index, true, true, window, cx);
386 }
387 }))
388 .child(item.tab_content(
389 TabContentParams {
390 selected,
391 deemphasized,
392 ..Default::default()
393 },
394 window,
395 cx,
396 ))
397 .on_drop(cx.listener(
398 move |this, dragged_tab: &DraggedTab, window, cx| {
399 this.drag_split_direction = None;
400 this.handle_tab_drop(dragged_tab, ix, window, cx)
401 },
402 ))
403 .on_drag(
404 DraggedTab {
405 item: item.boxed_clone(),
406 pane: cx.entity().clone(),
407 detail: 0,
408 is_active: selected,
409 ix,
410 },
411 |tab, _, _, cx| cx.new(|_| tab.clone()),
412 )
413 })),
414 )
415 .child({
416 let zoomed = pane.is_zoomed();
417 div()
418 .visible_on_hover(pane_group_id)
419 .when(is_hovered, |this| this.visible())
420 .child(
421 IconButton::new(
422 SharedString::from(format!(
423 "debug-toggle-zoom-{}",
424 cx.entity_id()
425 )),
426 if zoomed {
427 IconName::Minimize
428 } else {
429 IconName::Maximize
430 },
431 )
432 .icon_size(IconSize::XSmall)
433 .on_click(cx.listener(move |pane, _, window, cx| {
434 pane.toggle_zoom(&workspace::ToggleZoom, window, cx);
435 }))
436 .tooltip({
437 let focus_handle = focus_handle.clone();
438 move |window, cx| {
439 let zoomed_text =
440 if zoomed { "Zoom Out" } else { "Zoom In" };
441 Tooltip::for_action_in(
442 zoomed_text,
443 &workspace::ToggleZoom,
444 &focus_handle,
445 window,
446 cx,
447 )
448 }
449 }),
450 )
451 })
452 .into_any_element()
453 }
454 });
455 pane
456 });
457
458 ret
459}
460
461pub struct DebugTerminal {
462 pub terminal: Option<Entity<TerminalView>>,
463 focus_handle: FocusHandle,
464}
465
466impl DebugTerminal {
467 fn empty(cx: &mut Context<Self>) -> Self {
468 Self {
469 terminal: None,
470 focus_handle: cx.focus_handle(),
471 }
472 }
473}
474
475impl gpui::Render for DebugTerminal {
476 fn render(&mut self, _window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
477 if let Some(terminal) = self.terminal.clone() {
478 terminal.into_any_element()
479 } else {
480 div().track_focus(&self.focus_handle).into_any_element()
481 }
482 }
483}
484impl Focusable for DebugTerminal {
485 fn focus_handle(&self, cx: &App) -> FocusHandle {
486 if let Some(terminal) = self.terminal.as_ref() {
487 return terminal.focus_handle(cx);
488 } else {
489 self.focus_handle.clone()
490 }
491 }
492}
493
494impl RunningState {
495 pub fn new(
496 session: Entity<Session>,
497 project: Entity<Project>,
498 workspace: WeakEntity<Workspace>,
499 serialized_pane_layout: Option<SerializedPaneLayout>,
500 window: &mut Window,
501 cx: &mut Context<Self>,
502 ) -> Self {
503 let focus_handle = cx.focus_handle();
504 let session_id = session.read(cx).session_id();
505 let weak_state = cx.weak_entity();
506 let stack_frame_list = cx.new(|cx| {
507 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
508 });
509
510 let debug_terminal = cx.new(DebugTerminal::empty);
511
512 let variable_list =
513 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
514
515 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
516
517 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
518
519 let console = cx.new(|cx| {
520 Console::new(
521 session.clone(),
522 stack_frame_list.clone(),
523 variable_list.clone(),
524 window,
525 cx,
526 )
527 });
528
529 let breakpoint_list = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
530
531 let _subscriptions = vec![
532 cx.observe(&module_list, |_, _, cx| cx.notify()),
533 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
534 match event {
535 SessionEvent::Stopped(thread_id) => {
536 this.workspace
537 .update(cx, |workspace, cx| {
538 workspace.open_panel::<crate::DebugPanel>(window, cx);
539 })
540 .log_err();
541
542 if let Some(thread_id) = thread_id {
543 this.select_thread(*thread_id, window, cx);
544 }
545 }
546 SessionEvent::Threads => {
547 let threads = this.session.update(cx, |this, cx| this.threads(cx));
548 this.select_current_thread(&threads, window, cx);
549 }
550 SessionEvent::CapabilitiesLoaded => {
551 let capabilities = this.capabilities(cx);
552 if !capabilities.supports_modules_request.unwrap_or(false) {
553 this.remove_pane_item(DebuggerPaneItem::Modules, window, cx);
554 }
555 if !capabilities
556 .supports_loaded_sources_request
557 .unwrap_or(false)
558 {
559 this.remove_pane_item(DebuggerPaneItem::LoadedSources, window, cx);
560 }
561 }
562
563 _ => {}
564 }
565 cx.notify()
566 }),
567 cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
568 this.serialize_layout(window, cx);
569 }),
570 ];
571
572 let mut pane_close_subscriptions = HashMap::default();
573 let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| {
574 persistence::deserialize_pane_layout(
575 serialized_layout,
576 &workspace,
577 &project,
578 &stack_frame_list,
579 &variable_list,
580 &module_list,
581 &console,
582 &breakpoint_list,
583 &loaded_source_list,
584 &debug_terminal,
585 &mut pane_close_subscriptions,
586 window,
587 cx,
588 )
589 }) {
590 workspace::PaneGroup::with_root(root)
591 } else {
592 pane_close_subscriptions.clear();
593
594 let root = Self::default_pane_layout(
595 project,
596 &workspace,
597 &stack_frame_list,
598 &variable_list,
599 &module_list,
600 &loaded_source_list,
601 &console,
602 &breakpoint_list,
603 &mut pane_close_subscriptions,
604 window,
605 cx,
606 );
607
608 workspace::PaneGroup::with_root(root)
609 };
610
611 Self {
612 session,
613 workspace,
614 focus_handle,
615 variable_list,
616 _subscriptions,
617 thread_id: None,
618 _remote_id: None,
619 stack_frame_list,
620 session_id,
621 panes,
622 active_pane: None,
623 module_list,
624 console,
625 breakpoint_list,
626 loaded_sources_list: loaded_source_list,
627 pane_close_subscriptions,
628 debug_terminal,
629 _schedule_serialize: None,
630 }
631 }
632
633 pub(crate) fn remove_pane_item(
634 &mut self,
635 item_kind: DebuggerPaneItem,
636 window: &mut Window,
637 cx: &mut Context<Self>,
638 ) {
639 if let Some((pane, item_id)) = self.panes.panes().iter().find_map(|pane| {
640 Some(pane).zip(
641 pane.read(cx)
642 .items()
643 .find(|item| {
644 item.act_as::<SubView>(cx)
645 .is_some_and(|view| view.read(cx).kind == item_kind)
646 })
647 .map(|item| item.item_id()),
648 )
649 }) {
650 pane.update(cx, |pane, cx| {
651 pane.remove_item(item_id, false, true, window, cx)
652 })
653 }
654 }
655
656 pub(crate) fn has_pane_at_position(&self, position: Point<Pixels>) -> bool {
657 self.panes.pane_at_pixel_position(position).is_some()
658 }
659
660 fn create_sub_view(
661 &self,
662 item_kind: DebuggerPaneItem,
663 _pane: &Entity<Pane>,
664 cx: &mut Context<Self>,
665 ) -> Box<dyn ItemHandle> {
666 match item_kind {
667 DebuggerPaneItem::Console => {
668 let weak_console = self.console.clone().downgrade();
669
670 Box::new(SubView::new(
671 self.console.focus_handle(cx),
672 self.console.clone().into(),
673 item_kind,
674 Some(Box::new(move |cx| {
675 weak_console
676 .read_with(cx, |console, cx| console.show_indicator(cx))
677 .unwrap_or_default()
678 })),
679 cx,
680 ))
681 }
682 DebuggerPaneItem::Variables => Box::new(SubView::new(
683 self.variable_list.focus_handle(cx),
684 self.variable_list.clone().into(),
685 item_kind,
686 None,
687 cx,
688 )),
689 DebuggerPaneItem::BreakpointList => Box::new(SubView::new(
690 self.breakpoint_list.focus_handle(cx),
691 self.breakpoint_list.clone().into(),
692 item_kind,
693 None,
694 cx,
695 )),
696 DebuggerPaneItem::Frames => Box::new(SubView::new(
697 self.stack_frame_list.focus_handle(cx),
698 self.stack_frame_list.clone().into(),
699 item_kind,
700 None,
701 cx,
702 )),
703 DebuggerPaneItem::Modules => Box::new(SubView::new(
704 self.module_list.focus_handle(cx),
705 self.module_list.clone().into(),
706 item_kind,
707 None,
708 cx,
709 )),
710 DebuggerPaneItem::LoadedSources => Box::new(SubView::new(
711 self.loaded_sources_list.focus_handle(cx),
712 self.loaded_sources_list.clone().into(),
713 item_kind,
714 None,
715 cx,
716 )),
717 DebuggerPaneItem::Terminal => Box::new(SubView::new(
718 self.debug_terminal.focus_handle(cx),
719 self.debug_terminal.clone().into(),
720 item_kind,
721 None,
722 cx,
723 )),
724 }
725 }
726
727 pub(crate) fn ensure_pane_item(
728 &mut self,
729 item_kind: DebuggerPaneItem,
730 window: &mut Window,
731 cx: &mut Context<Self>,
732 ) {
733 if self.pane_items_status(cx).get(&item_kind) == Some(&true) {
734 return;
735 };
736 let pane = self.panes.last_pane();
737 let sub_view = self.create_sub_view(item_kind, &pane, cx);
738
739 pane.update(cx, |pane, cx| {
740 pane.add_item_inner(sub_view, false, false, false, None, window, cx);
741 })
742 }
743
744 pub(crate) fn add_pane_item(
745 &mut self,
746 item_kind: DebuggerPaneItem,
747 position: Point<Pixels>,
748 window: &mut Window,
749 cx: &mut Context<Self>,
750 ) {
751 debug_assert!(
752 item_kind.is_supported(self.session.read(cx).capabilities()),
753 "We should only allow adding supported item kinds"
754 );
755
756 if let Some(pane) = self.panes.pane_at_pixel_position(position) {
757 let sub_view = self.create_sub_view(item_kind, pane, cx);
758
759 pane.update(cx, |pane, cx| {
760 pane.add_item(sub_view, false, false, None, window, cx);
761 })
762 }
763 }
764
765 pub(crate) fn pane_items_status(&self, cx: &App) -> IndexMap<DebuggerPaneItem, bool> {
766 let caps = self.session.read(cx).capabilities();
767 let mut pane_item_status = IndexMap::from_iter(
768 DebuggerPaneItem::all()
769 .iter()
770 .filter(|kind| kind.is_supported(&caps))
771 .map(|kind| (*kind, false)),
772 );
773 self.panes.panes().iter().for_each(|pane| {
774 pane.read(cx)
775 .items()
776 .filter_map(|item| item.act_as::<SubView>(cx))
777 .for_each(|view| {
778 pane_item_status.insert(view.read(cx).kind, true);
779 });
780 });
781
782 pane_item_status
783 }
784
785 pub(crate) fn serialize_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
786 if self._schedule_serialize.is_none() {
787 self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
788 cx.background_executor()
789 .timer(Duration::from_millis(100))
790 .await;
791
792 let Some((adapter_name, pane_group)) = this
793 .update(cx, |this, cx| {
794 let adapter_name = this.session.read(cx).adapter_name();
795 (
796 adapter_name,
797 persistence::build_serialized_pane_layout(&this.panes.root, cx),
798 )
799 })
800 .ok()
801 else {
802 return;
803 };
804
805 persistence::serialize_pane_layout(adapter_name, pane_group)
806 .await
807 .log_err();
808
809 this.update(cx, |this, _| {
810 this._schedule_serialize.take();
811 })
812 .ok();
813 }));
814 }
815 }
816
817 pub(crate) fn handle_pane_event(
818 this: &mut RunningState,
819 source_pane: &Entity<Pane>,
820 event: &Event,
821 window: &mut Window,
822 cx: &mut Context<RunningState>,
823 ) {
824 this.serialize_layout(window, cx);
825 match event {
826 Event::Remove { .. } => {
827 let _did_find_pane = this.panes.remove(&source_pane).is_ok();
828 debug_assert!(_did_find_pane);
829 cx.notify();
830 }
831 Event::Focus => {
832 this.active_pane = Some(source_pane.clone());
833 }
834 Event::ZoomIn => {
835 source_pane.update(cx, |pane, cx| {
836 pane.set_zoomed(true, cx);
837 });
838 cx.notify();
839 }
840 Event::ZoomOut => {
841 source_pane.update(cx, |pane, cx| {
842 pane.set_zoomed(false, cx);
843 });
844 cx.notify();
845 }
846 _ => {}
847 }
848 }
849
850 pub(crate) fn activate_pane_in_direction(
851 &mut self,
852 direction: SplitDirection,
853 window: &mut Window,
854 cx: &mut Context<Self>,
855 ) {
856 if let Some(pane) = self
857 .active_pane
858 .as_ref()
859 .and_then(|pane| self.panes.find_pane_in_direction(pane, direction, cx))
860 {
861 window.focus(&pane.focus_handle(cx));
862 } else {
863 self.workspace
864 .update(cx, |workspace, cx| {
865 workspace.activate_pane_in_direction(direction, window, cx)
866 })
867 .ok();
868 }
869 }
870
871 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
872 if self.thread_id.is_some() {
873 self.stack_frame_list
874 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
875 }
876 }
877
878 pub(crate) fn has_open_context_menu(&self, cx: &App) -> bool {
879 self.variable_list.read(cx).has_open_context_menu()
880 }
881
882 pub fn session(&self) -> &Entity<Session> {
883 &self.session
884 }
885
886 pub fn session_id(&self) -> SessionId {
887 self.session_id
888 }
889
890 pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
891 self.stack_frame_list.read(cx).selected_stack_frame_id()
892 }
893
894 #[cfg(test)]
895 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
896 &self.stack_frame_list
897 }
898
899 #[cfg(test)]
900 pub fn console(&self) -> &Entity<Console> {
901 &self.console
902 }
903
904 #[cfg(test)]
905 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
906 &self.module_list
907 }
908
909 pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) {
910 let (variable_list_position, pane) = self
911 .panes
912 .panes()
913 .into_iter()
914 .find_map(|pane| {
915 pane.read(cx)
916 .items_of_type::<SubView>()
917 .position(|view| view.read(cx).view_kind() == item)
918 .map(|view| (view, pane))
919 })
920 .unwrap();
921 pane.update(cx, |this, cx| {
922 this.activate_item(variable_list_position, true, true, window, cx);
923 })
924 }
925
926 #[cfg(test)]
927 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
928 &self.variable_list
929 }
930
931 pub fn capabilities(&self, cx: &App) -> Capabilities {
932 self.session().read(cx).capabilities().clone()
933 }
934
935 pub fn select_current_thread(
936 &mut self,
937 threads: &Vec<(Thread, ThreadStatus)>,
938 window: &mut Window,
939 cx: &mut Context<Self>,
940 ) {
941 let selected_thread = self
942 .thread_id
943 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
944 .or_else(|| threads.first());
945
946 let Some((selected_thread, _)) = selected_thread else {
947 return;
948 };
949
950 if Some(ThreadId(selected_thread.id)) != self.thread_id {
951 self.select_thread(ThreadId(selected_thread.id), window, cx);
952 }
953 }
954
955 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
956 self.thread_id
957 }
958
959 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
960 self.thread_id
961 .map(|id| self.session().read(cx).thread_status(id))
962 }
963
964 fn select_thread(&mut self, thread_id: ThreadId, window: &mut Window, cx: &mut Context<Self>) {
965 if self.thread_id.is_some_and(|id| id == thread_id) {
966 return;
967 }
968
969 self.thread_id = Some(thread_id);
970
971 self.stack_frame_list
972 .update(cx, |list, cx| list.schedule_refresh(true, window, cx));
973 }
974
975 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
976 let Some(thread_id) = self.thread_id else {
977 return;
978 };
979
980 self.session().update(cx, |state, cx| {
981 state.continue_thread(thread_id, cx);
982 });
983 }
984
985 pub fn step_over(&mut self, cx: &mut Context<Self>) {
986 let Some(thread_id) = self.thread_id else {
987 return;
988 };
989
990 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
991
992 self.session().update(cx, |state, cx| {
993 state.step_over(thread_id, granularity, cx);
994 });
995 }
996
997 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
998 let Some(thread_id) = self.thread_id else {
999 return;
1000 };
1001
1002 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1003
1004 self.session().update(cx, |state, cx| {
1005 state.step_in(thread_id, granularity, cx);
1006 });
1007 }
1008
1009 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
1010 let Some(thread_id) = self.thread_id else {
1011 return;
1012 };
1013
1014 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1015
1016 self.session().update(cx, |state, cx| {
1017 state.step_out(thread_id, granularity, cx);
1018 });
1019 }
1020
1021 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
1022 let Some(thread_id) = self.thread_id else {
1023 return;
1024 };
1025
1026 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
1027
1028 self.session().update(cx, |state, cx| {
1029 state.step_back(thread_id, granularity, cx);
1030 });
1031 }
1032
1033 pub fn restart_session(&self, cx: &mut Context<Self>) {
1034 self.session().update(cx, |state, cx| {
1035 state.restart(None, cx);
1036 });
1037 }
1038
1039 pub fn pause_thread(&self, cx: &mut Context<Self>) {
1040 let Some(thread_id) = self.thread_id else {
1041 return;
1042 };
1043
1044 self.session().update(cx, |state, cx| {
1045 state.pause_thread(thread_id, cx);
1046 });
1047 }
1048
1049 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
1050 self.workspace
1051 .update(cx, |workspace, cx| {
1052 workspace
1053 .project()
1054 .read(cx)
1055 .breakpoint_store()
1056 .update(cx, |store, cx| {
1057 store.remove_active_position(Some(self.session_id), cx)
1058 })
1059 })
1060 .log_err();
1061
1062 self.session.update(cx, |session, cx| {
1063 session.shutdown(cx).detach();
1064 })
1065 }
1066
1067 pub fn stop_thread(&self, cx: &mut Context<Self>) {
1068 let Some(thread_id) = self.thread_id else {
1069 return;
1070 };
1071
1072 self.workspace
1073 .update(cx, |workspace, cx| {
1074 workspace
1075 .project()
1076 .read(cx)
1077 .breakpoint_store()
1078 .update(cx, |store, cx| {
1079 store.remove_active_position(Some(self.session_id), cx)
1080 })
1081 })
1082 .log_err();
1083
1084 self.session().update(cx, |state, cx| {
1085 state.terminate_threads(Some(vec![thread_id; 1]), cx);
1086 });
1087 }
1088
1089 #[expect(
1090 unused,
1091 reason = "Support for disconnecting a client is not wired through yet"
1092 )]
1093 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
1094 self.session().update(cx, |state, cx| {
1095 state.disconnect_client(cx);
1096 });
1097 }
1098
1099 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
1100 self.session.update(cx, |session, cx| {
1101 session.toggle_ignore_breakpoints(cx).detach();
1102 });
1103 }
1104
1105 pub(crate) fn thread_dropdown(
1106 &self,
1107 window: &mut Window,
1108 cx: &mut Context<'_, RunningState>,
1109 ) -> DropdownMenu {
1110 let state = cx.entity();
1111 let threads = self.session.update(cx, |this, cx| this.threads(cx));
1112 let selected_thread_name = threads
1113 .iter()
1114 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
1115 .map(|(thread, _)| thread.name.clone())
1116 .unwrap_or("Threads".to_owned());
1117 DropdownMenu::new(
1118 ("thread-list", self.session_id.0),
1119 selected_thread_name,
1120 ContextMenu::build_eager(window, cx, move |mut this, _, _| {
1121 for (thread, _) in threads {
1122 let state = state.clone();
1123 let thread_id = thread.id;
1124 this = this.entry(thread.name, None, move |window, cx| {
1125 state.update(cx, |state, cx| {
1126 state.select_thread(ThreadId(thread_id), window, cx);
1127 });
1128 });
1129 }
1130 this
1131 }),
1132 )
1133 }
1134
1135 fn default_pane_layout(
1136 project: Entity<Project>,
1137 workspace: &WeakEntity<Workspace>,
1138 stack_frame_list: &Entity<StackFrameList>,
1139 variable_list: &Entity<VariableList>,
1140 module_list: &Entity<ModuleList>,
1141 loaded_source_list: &Entity<LoadedSourceList>,
1142 console: &Entity<Console>,
1143 breakpoints: &Entity<BreakpointList>,
1144 subscriptions: &mut HashMap<EntityId, Subscription>,
1145 window: &mut Window,
1146 cx: &mut Context<'_, RunningState>,
1147 ) -> Member {
1148 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1149 leftmost_pane.update(cx, |this, cx| {
1150 this.add_item(
1151 Box::new(SubView::new(
1152 this.focus_handle(cx),
1153 stack_frame_list.clone().into(),
1154 DebuggerPaneItem::Frames,
1155 None,
1156 cx,
1157 )),
1158 true,
1159 false,
1160 None,
1161 window,
1162 cx,
1163 );
1164 this.add_item(
1165 Box::new(SubView::new(
1166 breakpoints.focus_handle(cx),
1167 breakpoints.clone().into(),
1168 DebuggerPaneItem::BreakpointList,
1169 None,
1170 cx,
1171 )),
1172 true,
1173 false,
1174 None,
1175 window,
1176 cx,
1177 );
1178 this.activate_item(0, false, false, window, cx);
1179 });
1180 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1181
1182 center_pane.update(cx, |this, cx| {
1183 this.add_item(
1184 Box::new(SubView::new(
1185 variable_list.focus_handle(cx),
1186 variable_list.clone().into(),
1187 DebuggerPaneItem::Variables,
1188 None,
1189 cx,
1190 )),
1191 true,
1192 false,
1193 None,
1194 window,
1195 cx,
1196 );
1197 this.add_item(
1198 Box::new(SubView::new(
1199 module_list.focus_handle(cx),
1200 module_list.clone().into(),
1201 DebuggerPaneItem::Modules,
1202 None,
1203 cx,
1204 )),
1205 false,
1206 false,
1207 None,
1208 window,
1209 cx,
1210 );
1211
1212 this.add_item(
1213 Box::new(SubView::new(
1214 loaded_source_list.focus_handle(cx),
1215 loaded_source_list.clone().into(),
1216 DebuggerPaneItem::LoadedSources,
1217 None,
1218 cx,
1219 )),
1220 false,
1221 false,
1222 None,
1223 window,
1224 cx,
1225 );
1226 this.activate_item(0, false, false, window, cx);
1227 });
1228
1229 let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
1230 rightmost_pane.update(cx, |this, cx| {
1231 let weak_console = console.downgrade();
1232 this.add_item(
1233 Box::new(SubView::new(
1234 this.focus_handle(cx),
1235 console.clone().into(),
1236 DebuggerPaneItem::Console,
1237 Some(Box::new(move |cx| {
1238 weak_console
1239 .read_with(cx, |console, cx| console.show_indicator(cx))
1240 .unwrap_or_default()
1241 })),
1242 cx,
1243 )),
1244 true,
1245 false,
1246 None,
1247 window,
1248 cx,
1249 );
1250 });
1251
1252 subscriptions.extend(
1253 [&leftmost_pane, ¢er_pane, &rightmost_pane]
1254 .into_iter()
1255 .map(|entity| {
1256 (
1257 entity.entity_id(),
1258 cx.subscribe_in(entity, window, Self::handle_pane_event),
1259 )
1260 }),
1261 );
1262
1263 let group_root = workspace::PaneAxis::new(
1264 gpui::Axis::Horizontal,
1265 [leftmost_pane, center_pane, rightmost_pane]
1266 .into_iter()
1267 .map(workspace::Member::Pane)
1268 .collect(),
1269 );
1270
1271 Member::Axis(group_root)
1272 }
1273}
1274
1275impl EventEmitter<DebugPanelItemEvent> for RunningState {}
1276
1277impl Focusable for RunningState {
1278 fn focus_handle(&self, _: &App) -> FocusHandle {
1279 self.focus_handle.clone()
1280 }
1281}