1mod breakpoint_list;
2mod console;
3mod loaded_source_list;
4mod module_list;
5pub mod stack_frame_list;
6pub mod variable_list;
7
8use std::{any::Any, ops::ControlFlow, sync::Arc};
9
10use super::DebugPanelItemEvent;
11use breakpoint_list::BreakpointList;
12use collections::HashMap;
13use console::Console;
14use dap::{Capabilities, Thread, client::SessionId, debugger_settings::DebuggerSettings};
15use gpui::{
16 Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
17 NoAction, Subscription, WeakEntity,
18};
19use loaded_source_list::LoadedSourceList;
20use module_list::ModuleList;
21use project::{
22 Project,
23 debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
24};
25use rpc::proto::ViewId;
26use settings::Settings;
27use stack_frame_list::StackFrameList;
28use ui::{
29 AnyElement, App, Context, ContextMenu, DropdownMenu, InteractiveElement, IntoElement, Label,
30 LabelCommon as _, ParentElement, Render, SharedString, Styled, Window, div, h_flex, v_flex,
31};
32use util::ResultExt;
33use variable_list::VariableList;
34use workspace::{
35 ActivePaneDecorator, DraggedTab, Item, Pane, PaneGroup, Workspace, move_item, pane::Event,
36};
37
38pub struct RunningState {
39 session: Entity<Session>,
40 thread_id: Option<ThreadId>,
41 focus_handle: FocusHandle,
42 _remote_id: Option<ViewId>,
43 workspace: WeakEntity<Workspace>,
44 session_id: SessionId,
45 variable_list: Entity<variable_list::VariableList>,
46 _subscriptions: Vec<Subscription>,
47 stack_frame_list: Entity<stack_frame_list::StackFrameList>,
48 _module_list: Entity<module_list::ModuleList>,
49 _console: Entity<Console>,
50 panes: PaneGroup,
51 pane_close_subscriptions: HashMap<EntityId, Subscription>,
52}
53
54impl Render for RunningState {
55 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
56 let active = self.panes.panes().into_iter().next();
57 let x = if let Some(active) = active {
58 self.panes
59 .render(
60 None,
61 &ActivePaneDecorator::new(active, &self.workspace),
62 window,
63 cx,
64 )
65 .into_any_element()
66 } else {
67 div().into_any_element()
68 };
69 let thread_status = self
70 .thread_id
71 .map(|thread_id| self.session.read(cx).thread_status(thread_id))
72 .unwrap_or(ThreadStatus::Exited);
73
74 self.variable_list.update(cx, |this, cx| {
75 this.disabled(thread_status != ThreadStatus::Stopped, cx);
76 });
77 v_flex()
78 .size_full()
79 .key_context("DebugSessionItem")
80 .track_focus(&self.focus_handle(cx))
81 .child(h_flex().flex_1().child(x))
82 }
83}
84
85struct SubView {
86 inner: AnyView,
87 pane_focus_handle: FocusHandle,
88 tab_name: SharedString,
89 show_indicator: Box<dyn Fn(&App) -> bool>,
90}
91
92impl SubView {
93 fn new(
94 pane_focus_handle: FocusHandle,
95 view: AnyView,
96 tab_name: SharedString,
97 show_indicator: Option<Box<dyn Fn(&App) -> bool>>,
98 cx: &mut App,
99 ) -> Entity<Self> {
100 cx.new(|_| Self {
101 tab_name,
102 inner: view,
103 pane_focus_handle,
104 show_indicator: show_indicator.unwrap_or(Box::new(|_| false)),
105 })
106 }
107}
108impl Focusable for SubView {
109 fn focus_handle(&self, _: &App) -> FocusHandle {
110 self.pane_focus_handle.clone()
111 }
112}
113impl EventEmitter<()> for SubView {}
114impl Item for SubView {
115 type Event = ();
116
117 fn tab_content(
118 &self,
119 params: workspace::item::TabContentParams,
120 _: &Window,
121 cx: &App,
122 ) -> AnyElement {
123 let label = Label::new(self.tab_name.clone())
124 .color(params.text_color())
125 .into_any_element();
126
127 if !params.selected && self.show_indicator.as_ref()(cx) {
128 return h_flex()
129 .justify_between()
130 .child(ui::Indicator::dot())
131 .gap_2()
132 .child(label)
133 .into_any_element();
134 }
135
136 label
137 }
138}
139
140impl Render for SubView {
141 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
142 v_flex().size_full().child(self.inner.clone())
143 }
144}
145
146fn new_debugger_pane(
147 workspace: WeakEntity<Workspace>,
148 project: Entity<Project>,
149 window: &mut Window,
150 cx: &mut Context<RunningState>,
151) -> Entity<Pane> {
152 let weak_running = cx.weak_entity();
153 let custom_drop_handle = {
154 let workspace = workspace.clone();
155 let project = project.downgrade();
156 let weak_running = weak_running.clone();
157 move |pane: &mut Pane, any: &dyn Any, window: &mut Window, cx: &mut Context<Pane>| {
158 let Some(tab) = any.downcast_ref::<DraggedTab>() else {
159 return ControlFlow::Break(());
160 };
161 let Some(project) = project.upgrade() else {
162 return ControlFlow::Break(());
163 };
164 let this_pane = cx.entity().clone();
165 let item = if tab.pane == this_pane {
166 pane.item_for_index(tab.ix)
167 } else {
168 tab.pane.read(cx).item_for_index(tab.ix)
169 };
170 let Some(item) = item.filter(|item| item.downcast::<SubView>().is_some()) else {
171 return ControlFlow::Break(());
172 };
173
174 let source = tab.pane.clone();
175 let item_id_to_move = item.item_id();
176
177 let Ok(new_split_pane) = pane
178 .drag_split_direction()
179 .map(|split_direction| {
180 weak_running.update(cx, |running, cx| {
181 let new_pane =
182 new_debugger_pane(workspace.clone(), project.clone(), window, cx);
183 let _previous_subscription = running.pane_close_subscriptions.insert(
184 new_pane.entity_id(),
185 cx.subscribe(&new_pane, RunningState::handle_pane_event),
186 );
187 debug_assert!(_previous_subscription.is_none());
188 running
189 .panes
190 .split(&this_pane, &new_pane, split_direction)?;
191 anyhow::Ok(new_pane)
192 })
193 })
194 .transpose()
195 else {
196 return ControlFlow::Break(());
197 };
198
199 match new_split_pane.transpose() {
200 // Source pane may be the one currently updated, so defer the move.
201 Ok(Some(new_pane)) => cx
202 .spawn_in(window, async move |_, cx| {
203 cx.update(|window, cx| {
204 move_item(
205 &source,
206 &new_pane,
207 item_id_to_move,
208 new_pane.read(cx).active_item_index(),
209 window,
210 cx,
211 );
212 })
213 .ok();
214 })
215 .detach(),
216 // If we drop into existing pane or current pane,
217 // regular pane drop handler will take care of it,
218 // using the right tab index for the operation.
219 Ok(None) => return ControlFlow::Continue(()),
220 err @ Err(_) => {
221 err.log_err();
222 return ControlFlow::Break(());
223 }
224 };
225
226 ControlFlow::Break(())
227 }
228 };
229
230 let ret = cx.new(move |cx| {
231 let mut pane = Pane::new(
232 workspace.clone(),
233 project.clone(),
234 Default::default(),
235 None,
236 NoAction.boxed_clone(),
237 window,
238 cx,
239 );
240 pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| {
241 if let Some(tab) = dragged_item.downcast_ref::<DraggedTab>() {
242 let is_current_pane = tab.pane == cx.entity();
243 let Some(can_drag_away) = weak_running
244 .update(cx, |running_state, _| {
245 let current_panes = running_state.panes.panes();
246 !current_panes.contains(&&tab.pane)
247 || current_panes.len() > 1
248 || (!is_current_pane || pane.items_len() > 1)
249 })
250 .ok()
251 else {
252 return false;
253 };
254 if can_drag_away {
255 let item = if is_current_pane {
256 pane.item_for_index(tab.ix)
257 } else {
258 tab.pane.read(cx).item_for_index(tab.ix)
259 };
260 if let Some(item) = item {
261 return item.downcast::<SubView>().is_some();
262 }
263 }
264 }
265 false
266 })));
267 pane.display_nav_history_buttons(None);
268 pane.set_custom_drop_handle(cx, custom_drop_handle);
269 pane.set_render_tab_bar_buttons(cx, |_, _, _| (None, None));
270 pane
271 });
272
273 ret
274}
275impl RunningState {
276 pub fn new(
277 session: Entity<Session>,
278 project: Entity<Project>,
279 workspace: WeakEntity<Workspace>,
280 window: &mut Window,
281 cx: &mut Context<Self>,
282 ) -> Self {
283 let focus_handle = cx.focus_handle();
284 let session_id = session.read(cx).session_id();
285 let weak_state = cx.weak_entity();
286 let stack_frame_list = cx.new(|cx| {
287 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
288 });
289
290 let variable_list =
291 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
292
293 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
294
295 #[expect(unused)]
296 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
297
298 let console = cx.new(|cx| {
299 Console::new(
300 session.clone(),
301 stack_frame_list.clone(),
302 variable_list.clone(),
303 window,
304 cx,
305 )
306 });
307
308 let _subscriptions = vec![
309 cx.observe(&module_list, |_, _, cx| cx.notify()),
310 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
311 match event {
312 SessionEvent::Stopped(thread_id) => {
313 this.workspace
314 .update(cx, |workspace, cx| {
315 workspace.open_panel::<crate::DebugPanel>(window, cx);
316 })
317 .log_err();
318
319 if let Some(thread_id) = thread_id {
320 this.select_thread(*thread_id, cx);
321 }
322 }
323 SessionEvent::Threads => {
324 let threads = this.session.update(cx, |this, cx| this.threads(cx));
325 this.select_current_thread(&threads, cx);
326 }
327 _ => {}
328 }
329 cx.notify()
330 }),
331 ];
332
333 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
334 leftmost_pane.update(cx, |this, cx| {
335 this.add_item(
336 Box::new(SubView::new(
337 this.focus_handle(cx),
338 stack_frame_list.clone().into(),
339 SharedString::new_static("Frames"),
340 None,
341 cx,
342 )),
343 true,
344 false,
345 None,
346 window,
347 cx,
348 );
349 let breakpoints = BreakpointList::new(session.clone(), workspace.clone(), &project, cx);
350 this.add_item(
351 Box::new(SubView::new(
352 breakpoints.focus_handle(cx),
353 breakpoints.into(),
354 SharedString::new_static("Breakpoints"),
355 None,
356 cx,
357 )),
358 true,
359 false,
360 None,
361 window,
362 cx,
363 );
364 this.activate_item(0, false, false, window, cx);
365 });
366 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
367 center_pane.update(cx, |this, cx| {
368 this.add_item(
369 Box::new(SubView::new(
370 variable_list.focus_handle(cx),
371 variable_list.clone().into(),
372 SharedString::new_static("Variables"),
373 None,
374 cx,
375 )),
376 true,
377 false,
378 None,
379 window,
380 cx,
381 );
382 this.add_item(
383 Box::new(SubView::new(
384 this.focus_handle(cx),
385 module_list.clone().into(),
386 SharedString::new_static("Modules"),
387 None,
388 cx,
389 )),
390 false,
391 false,
392 None,
393 window,
394 cx,
395 );
396 this.activate_item(0, false, false, window, cx);
397 });
398 let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
399 rightmost_pane.update(cx, |this, cx| {
400 let weak_console = console.downgrade();
401 this.add_item(
402 Box::new(SubView::new(
403 this.focus_handle(cx),
404 console.clone().into(),
405 SharedString::new_static("Console"),
406 Some(Box::new(move |cx| {
407 weak_console
408 .read_with(cx, |console, cx| console.show_indicator(cx))
409 .unwrap_or_default()
410 })),
411 cx,
412 )),
413 true,
414 false,
415 None,
416 window,
417 cx,
418 );
419 });
420 let pane_close_subscriptions = HashMap::from_iter(
421 [&leftmost_pane, ¢er_pane, &rightmost_pane]
422 .into_iter()
423 .map(|entity| {
424 (
425 entity.entity_id(),
426 cx.subscribe(entity, Self::handle_pane_event),
427 )
428 }),
429 );
430 let group_root = workspace::PaneAxis::new(
431 gpui::Axis::Horizontal,
432 [leftmost_pane, center_pane, rightmost_pane]
433 .into_iter()
434 .map(workspace::Member::Pane)
435 .collect(),
436 );
437
438 let panes = PaneGroup::with_root(workspace::Member::Axis(group_root));
439
440 Self {
441 session,
442 workspace,
443 focus_handle,
444 variable_list,
445 _subscriptions,
446 thread_id: None,
447 _remote_id: None,
448 stack_frame_list,
449 session_id,
450 panes,
451 _module_list: module_list,
452 _console: console,
453 pane_close_subscriptions,
454 }
455 }
456
457 fn handle_pane_event(
458 this: &mut RunningState,
459 source_pane: Entity<Pane>,
460 event: &Event,
461 cx: &mut Context<RunningState>,
462 ) {
463 if let Event::Remove { .. } = event {
464 let _did_find_pane = this.panes.remove(&source_pane).is_ok();
465 debug_assert!(_did_find_pane);
466 cx.notify();
467 }
468 }
469 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
470 if self.thread_id.is_some() {
471 self.stack_frame_list
472 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
473 }
474 }
475
476 pub fn session(&self) -> &Entity<Session> {
477 &self.session
478 }
479
480 pub fn session_id(&self) -> SessionId {
481 self.session_id
482 }
483
484 pub(crate) fn selected_stack_frame_id(&self, cx: &App) -> Option<dap::StackFrameId> {
485 self.stack_frame_list.read(cx).selected_stack_frame_id()
486 }
487
488 #[cfg(test)]
489 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
490 &self.stack_frame_list
491 }
492
493 #[cfg(test)]
494 pub fn console(&self) -> &Entity<Console> {
495 &self._console
496 }
497
498 #[cfg(test)]
499 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
500 &self._module_list
501 }
502
503 #[cfg(test)]
504 pub(crate) fn activate_modules_list(&self, window: &mut Window, cx: &mut App) {
505 let (variable_list_position, pane) = self
506 .panes
507 .panes()
508 .into_iter()
509 .find_map(|pane| {
510 pane.read(cx)
511 .items_of_type::<SubView>()
512 .position(|view| view.read(cx).tab_name == *"Modules")
513 .map(|view| (view, pane))
514 })
515 .unwrap();
516 pane.update(cx, |this, cx| {
517 this.activate_item(variable_list_position, true, true, window, cx);
518 })
519 }
520 #[cfg(test)]
521 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
522 &self.variable_list
523 }
524
525 pub fn capabilities(&self, cx: &App) -> Capabilities {
526 self.session().read(cx).capabilities().clone()
527 }
528
529 pub fn select_current_thread(
530 &mut self,
531 threads: &Vec<(Thread, ThreadStatus)>,
532 cx: &mut Context<Self>,
533 ) {
534 let selected_thread = self
535 .thread_id
536 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
537 .or_else(|| threads.first());
538
539 let Some((selected_thread, _)) = selected_thread else {
540 return;
541 };
542
543 if Some(ThreadId(selected_thread.id)) != self.thread_id {
544 self.select_thread(ThreadId(selected_thread.id), cx);
545 }
546 }
547
548 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
549 self.thread_id
550 }
551
552 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
553 self.thread_id
554 .map(|id| self.session().read(cx).thread_status(id))
555 }
556
557 fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
558 if self.thread_id.is_some_and(|id| id == thread_id) {
559 return;
560 }
561
562 self.thread_id = Some(thread_id);
563
564 self.stack_frame_list
565 .update(cx, |list, cx| list.refresh(cx));
566 cx.notify();
567 }
568
569 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
570 let Some(thread_id) = self.thread_id else {
571 return;
572 };
573
574 self.session().update(cx, |state, cx| {
575 state.continue_thread(thread_id, cx);
576 });
577 }
578
579 pub fn step_over(&mut self, cx: &mut Context<Self>) {
580 let Some(thread_id) = self.thread_id else {
581 return;
582 };
583
584 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
585
586 self.session().update(cx, |state, cx| {
587 state.step_over(thread_id, granularity, cx);
588 });
589 }
590
591 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
592 let Some(thread_id) = self.thread_id else {
593 return;
594 };
595
596 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
597
598 self.session().update(cx, |state, cx| {
599 state.step_in(thread_id, granularity, cx);
600 });
601 }
602
603 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
604 let Some(thread_id) = self.thread_id else {
605 return;
606 };
607
608 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
609
610 self.session().update(cx, |state, cx| {
611 state.step_out(thread_id, granularity, cx);
612 });
613 }
614
615 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
616 let Some(thread_id) = self.thread_id else {
617 return;
618 };
619
620 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
621
622 self.session().update(cx, |state, cx| {
623 state.step_back(thread_id, granularity, cx);
624 });
625 }
626
627 pub fn restart_session(&self, cx: &mut Context<Self>) {
628 self.session().update(cx, |state, cx| {
629 state.restart(None, cx);
630 });
631 }
632
633 pub fn pause_thread(&self, cx: &mut Context<Self>) {
634 let Some(thread_id) = self.thread_id else {
635 return;
636 };
637
638 self.session().update(cx, |state, cx| {
639 state.pause_thread(thread_id, cx);
640 });
641 }
642
643 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
644 self.workspace
645 .update(cx, |workspace, cx| {
646 workspace
647 .project()
648 .read(cx)
649 .breakpoint_store()
650 .update(cx, |store, cx| {
651 store.remove_active_position(Some(self.session_id), cx)
652 })
653 })
654 .log_err();
655
656 self.session.update(cx, |session, cx| {
657 session.shutdown(cx).detach();
658 })
659 }
660
661 pub fn stop_thread(&self, cx: &mut Context<Self>) {
662 let Some(thread_id) = self.thread_id else {
663 return;
664 };
665
666 self.workspace
667 .update(cx, |workspace, cx| {
668 workspace
669 .project()
670 .read(cx)
671 .breakpoint_store()
672 .update(cx, |store, cx| {
673 store.remove_active_position(Some(self.session_id), cx)
674 })
675 })
676 .log_err();
677
678 self.session().update(cx, |state, cx| {
679 state.terminate_threads(Some(vec![thread_id; 1]), cx);
680 });
681 }
682
683 #[expect(
684 unused,
685 reason = "Support for disconnecting a client is not wired through yet"
686 )]
687 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
688 self.session().update(cx, |state, cx| {
689 state.disconnect_client(cx);
690 });
691 }
692
693 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
694 self.session.update(cx, |session, cx| {
695 session.toggle_ignore_breakpoints(cx).detach();
696 });
697 }
698
699 pub(crate) fn thread_dropdown(
700 &self,
701 window: &mut Window,
702 cx: &mut Context<'_, RunningState>,
703 ) -> DropdownMenu {
704 let state = cx.entity();
705 let threads = self.session.update(cx, |this, cx| this.threads(cx));
706 let selected_thread_name = threads
707 .iter()
708 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
709 .map(|(thread, _)| thread.name.clone())
710 .unwrap_or("Threads".to_owned());
711 DropdownMenu::new(
712 ("thread-list", self.session_id.0),
713 selected_thread_name,
714 ContextMenu::build(window, cx, move |mut this, _, _| {
715 for (thread, _) in threads {
716 let state = state.clone();
717 let thread_id = thread.id;
718 this = this.entry(thread.name, None, move |_, cx| {
719 state.update(cx, |state, cx| {
720 state.select_thread(ThreadId(thread_id), cx);
721 });
722 });
723 }
724 this
725 }),
726 )
727 }
728}
729
730impl EventEmitter<DebugPanelItemEvent> for RunningState {}
731
732impl Focusable for RunningState {
733 fn focus_handle(&self, _: &App) -> FocusHandle {
734 self.focus_handle.clone()
735 }
736}