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