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
246 });
247
248 ret
249}
250impl RunningState {
251 pub fn new(
252 session: Entity<Session>,
253 project: Entity<Project>,
254 workspace: WeakEntity<Workspace>,
255 window: &mut Window,
256 cx: &mut Context<Self>,
257 ) -> Self {
258 let focus_handle = cx.focus_handle();
259 let session_id = session.read(cx).session_id();
260 let weak_state = cx.weak_entity();
261 let stack_frame_list = cx.new(|cx| {
262 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
263 });
264
265 let variable_list =
266 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
267
268 let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx));
269
270 #[expect(unused)]
271 let loaded_source_list = cx.new(|cx| LoadedSourceList::new(session.clone(), cx));
272
273 let console = cx.new(|cx| {
274 Console::new(
275 session.clone(),
276 stack_frame_list.clone(),
277 variable_list.clone(),
278 window,
279 cx,
280 )
281 });
282
283 let _subscriptions = vec![
284 cx.observe(&module_list, |_, _, cx| cx.notify()),
285 cx.subscribe_in(&session, window, |this, _, event, window, cx| {
286 match event {
287 SessionEvent::Stopped(thread_id) => {
288 this.workspace
289 .update(cx, |workspace, cx| {
290 workspace.open_panel::<crate::DebugPanel>(window, cx);
291 })
292 .log_err();
293
294 if let Some(thread_id) = thread_id {
295 this.select_thread(*thread_id, cx);
296 }
297 }
298 SessionEvent::Threads => {
299 let threads = this.session.update(cx, |this, cx| this.threads(cx));
300 this.select_current_thread(&threads, cx);
301 }
302 _ => {}
303 }
304 cx.notify()
305 }),
306 ];
307
308 let leftmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
309 leftmost_pane.update(cx, |this, cx| {
310 this.add_item(
311 Box::new(SubView::new(
312 this.focus_handle(cx),
313 stack_frame_list.clone().into(),
314 SharedString::new_static("Frames"),
315 cx,
316 )),
317 true,
318 false,
319 None,
320 window,
321 cx,
322 );
323 });
324 let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
325 center_pane.update(cx, |this, cx| {
326 this.add_item(
327 Box::new(SubView::new(
328 variable_list.focus_handle(cx),
329 variable_list.clone().into(),
330 SharedString::new_static("Variables"),
331 cx,
332 )),
333 true,
334 false,
335 None,
336 window,
337 cx,
338 );
339 this.add_item(
340 Box::new(SubView::new(
341 this.focus_handle(cx),
342 module_list.clone().into(),
343 SharedString::new_static("Modules"),
344 cx,
345 )),
346 true,
347 false,
348 None,
349 window,
350 cx,
351 );
352 });
353 let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
354 rightmost_pane.update(cx, |this, cx| {
355 this.add_item(
356 Box::new(SubView::new(
357 this.focus_handle(cx),
358 console.clone().into(),
359 SharedString::new_static("Console"),
360 cx,
361 )),
362 true,
363 false,
364 None,
365 window,
366 cx,
367 );
368 });
369 let pane_close_subscriptions = HashMap::from_iter(
370 [&leftmost_pane, ¢er_pane, &rightmost_pane]
371 .into_iter()
372 .map(|entity| {
373 (
374 entity.entity_id(),
375 cx.subscribe(entity, Self::handle_pane_event),
376 )
377 }),
378 );
379 let group_root = workspace::PaneAxis::new(
380 gpui::Axis::Horizontal,
381 [leftmost_pane, center_pane, rightmost_pane]
382 .into_iter()
383 .map(workspace::Member::Pane)
384 .collect(),
385 );
386
387 let panes = PaneGroup::with_root(workspace::Member::Axis(group_root));
388
389 Self {
390 session,
391 workspace,
392 focus_handle,
393 variable_list,
394 _subscriptions,
395 thread_id: None,
396 _remote_id: None,
397 stack_frame_list,
398 session_id,
399 panes,
400 _module_list: module_list,
401 _console: console,
402 pane_close_subscriptions,
403 }
404 }
405
406 fn handle_pane_event(
407 this: &mut RunningState,
408 source_pane: Entity<Pane>,
409 event: &Event,
410 cx: &mut Context<RunningState>,
411 ) {
412 if let Event::Remove { .. } = event {
413 let _did_find_pane = this.panes.remove(&source_pane).is_ok();
414 debug_assert!(_did_find_pane);
415 cx.notify();
416 }
417 }
418 pub(crate) fn go_to_selected_stack_frame(&self, window: &Window, cx: &mut Context<Self>) {
419 if self.thread_id.is_some() {
420 self.stack_frame_list
421 .update(cx, |list, cx| list.go_to_selected_stack_frame(window, cx));
422 }
423 }
424
425 pub fn session(&self) -> &Entity<Session> {
426 &self.session
427 }
428
429 pub fn session_id(&self) -> SessionId {
430 self.session_id
431 }
432
433 #[cfg(test)]
434 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
435 &self.stack_frame_list
436 }
437
438 #[cfg(test)]
439 pub fn console(&self) -> &Entity<Console> {
440 &self._console
441 }
442
443 #[cfg(test)]
444 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
445 &self._module_list
446 }
447
448 #[cfg(test)]
449 pub(crate) fn activate_variable_list(&self, window: &mut Window, cx: &mut App) {
450 let (variable_list_position, pane) = self
451 .panes
452 .panes()
453 .into_iter()
454 .find_map(|pane| {
455 pane.read(cx)
456 .items_of_type::<SubView>()
457 .position(|view| view.read(cx).tab_name == *"Variables")
458 .map(|view| (view, pane))
459 })
460 .unwrap();
461 pane.update(cx, |this, cx| {
462 this.activate_item(variable_list_position, true, true, window, cx);
463 })
464 }
465 #[cfg(test)]
466 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
467 &self.variable_list
468 }
469
470 pub fn capabilities(&self, cx: &App) -> Capabilities {
471 self.session().read(cx).capabilities().clone()
472 }
473
474 pub fn select_current_thread(
475 &mut self,
476 threads: &Vec<(Thread, ThreadStatus)>,
477 cx: &mut Context<Self>,
478 ) {
479 let selected_thread = self
480 .thread_id
481 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
482 .or_else(|| threads.first());
483
484 let Some((selected_thread, _)) = selected_thread else {
485 return;
486 };
487
488 if Some(ThreadId(selected_thread.id)) != self.thread_id {
489 self.select_thread(ThreadId(selected_thread.id), cx);
490 }
491 }
492
493 #[cfg(test)]
494 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
495 self.thread_id
496 }
497
498 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
499 self.thread_id
500 .map(|id| self.session().read(cx).thread_status(id))
501 }
502
503 fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
504 if self.thread_id.is_some_and(|id| id == thread_id) {
505 return;
506 }
507
508 self.thread_id = Some(thread_id);
509
510 self.stack_frame_list
511 .update(cx, |list, cx| list.refresh(cx));
512 cx.notify();
513 }
514
515 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
516 let Some(thread_id) = self.thread_id else {
517 return;
518 };
519
520 self.session().update(cx, |state, cx| {
521 state.continue_thread(thread_id, cx);
522 });
523 }
524
525 pub fn step_over(&mut self, cx: &mut Context<Self>) {
526 let Some(thread_id) = self.thread_id else {
527 return;
528 };
529
530 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
531
532 self.session().update(cx, |state, cx| {
533 state.step_over(thread_id, granularity, cx);
534 });
535 }
536
537 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
538 let Some(thread_id) = self.thread_id else {
539 return;
540 };
541
542 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
543
544 self.session().update(cx, |state, cx| {
545 state.step_in(thread_id, granularity, cx);
546 });
547 }
548
549 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
550 let Some(thread_id) = self.thread_id else {
551 return;
552 };
553
554 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
555
556 self.session().update(cx, |state, cx| {
557 state.step_out(thread_id, granularity, cx);
558 });
559 }
560
561 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
562 let Some(thread_id) = self.thread_id else {
563 return;
564 };
565
566 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
567
568 self.session().update(cx, |state, cx| {
569 state.step_back(thread_id, granularity, cx);
570 });
571 }
572
573 pub fn restart_session(&self, cx: &mut Context<Self>) {
574 self.session().update(cx, |state, cx| {
575 state.restart(None, cx);
576 });
577 }
578
579 pub fn pause_thread(&self, cx: &mut Context<Self>) {
580 let Some(thread_id) = self.thread_id else {
581 return;
582 };
583
584 self.session().update(cx, |state, cx| {
585 state.pause_thread(thread_id, cx);
586 });
587 }
588
589 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
590 self.workspace
591 .update(cx, |workspace, cx| {
592 workspace
593 .project()
594 .read(cx)
595 .breakpoint_store()
596 .update(cx, |store, cx| {
597 store.remove_active_position(Some(self.session_id), cx)
598 })
599 })
600 .log_err();
601
602 self.session.update(cx, |session, cx| {
603 session.shutdown(cx).detach();
604 })
605 }
606
607 pub fn stop_thread(&self, cx: &mut Context<Self>) {
608 let Some(thread_id) = self.thread_id else {
609 return;
610 };
611
612 self.workspace
613 .update(cx, |workspace, cx| {
614 workspace
615 .project()
616 .read(cx)
617 .breakpoint_store()
618 .update(cx, |store, cx| {
619 store.remove_active_position(Some(self.session_id), cx)
620 })
621 })
622 .log_err();
623
624 self.session().update(cx, |state, cx| {
625 state.terminate_threads(Some(vec![thread_id; 1]), cx);
626 });
627 }
628
629 #[expect(
630 unused,
631 reason = "Support for disconnecting a client is not wired through yet"
632 )]
633 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
634 self.session().update(cx, |state, cx| {
635 state.disconnect_client(cx);
636 });
637 }
638
639 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
640 self.session.update(cx, |session, cx| {
641 session.toggle_ignore_breakpoints(cx).detach();
642 });
643 }
644
645 pub(crate) fn thread_dropdown(
646 &self,
647 window: &mut Window,
648 cx: &mut Context<'_, RunningState>,
649 ) -> DropdownMenu {
650 let state = cx.entity();
651 let threads = self.session.update(cx, |this, cx| this.threads(cx));
652 let selected_thread_name = threads
653 .iter()
654 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
655 .map(|(thread, _)| thread.name.clone())
656 .unwrap_or("Threads".to_owned());
657 DropdownMenu::new(
658 ("thread-list", self.session_id.0),
659 selected_thread_name,
660 ContextMenu::build(window, cx, move |mut this, _, _| {
661 for (thread, _) in threads {
662 let state = state.clone();
663 let thread_id = thread.id;
664 this = this.entry(thread.name, None, move |_, cx| {
665 state.update(cx, |state, cx| {
666 state.select_thread(ThreadId(thread_id), cx);
667 });
668 });
669 }
670 this
671 }),
672 )
673 }
674}
675
676impl EventEmitter<DebugPanelItemEvent> for RunningState {}
677
678impl Focusable for RunningState {
679 fn focus_handle(&self, _: &App) -> FocusHandle {
680 self.focus_handle.clone()
681 }
682}