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