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 #[cfg(test)]
436 pub fn stack_frame_list(&self) -> &Entity<StackFrameList> {
437 &self.stack_frame_list
438 }
439
440 #[cfg(test)]
441 pub fn console(&self) -> &Entity<Console> {
442 &self._console
443 }
444
445 #[cfg(test)]
446 pub(crate) fn module_list(&self) -> &Entity<ModuleList> {
447 &self._module_list
448 }
449
450 #[cfg(test)]
451 pub(crate) fn activate_modules_list(&self, window: &mut Window, cx: &mut App) {
452 let (variable_list_position, pane) = self
453 .panes
454 .panes()
455 .into_iter()
456 .find_map(|pane| {
457 pane.read(cx)
458 .items_of_type::<SubView>()
459 .position(|view| view.read(cx).tab_name == *"Modules")
460 .map(|view| (view, pane))
461 })
462 .unwrap();
463 pane.update(cx, |this, cx| {
464 this.activate_item(variable_list_position, true, true, window, cx);
465 })
466 }
467 #[cfg(test)]
468 pub(crate) fn variable_list(&self) -> &Entity<VariableList> {
469 &self.variable_list
470 }
471
472 pub fn capabilities(&self, cx: &App) -> Capabilities {
473 self.session().read(cx).capabilities().clone()
474 }
475
476 pub fn select_current_thread(
477 &mut self,
478 threads: &Vec<(Thread, ThreadStatus)>,
479 cx: &mut Context<Self>,
480 ) {
481 let selected_thread = self
482 .thread_id
483 .and_then(|thread_id| threads.iter().find(|(thread, _)| thread.id == thread_id.0))
484 .or_else(|| threads.first());
485
486 let Some((selected_thread, _)) = selected_thread else {
487 return;
488 };
489
490 if Some(ThreadId(selected_thread.id)) != self.thread_id {
491 self.select_thread(ThreadId(selected_thread.id), cx);
492 }
493 }
494
495 #[cfg(test)]
496 pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> {
497 self.thread_id
498 }
499
500 pub fn thread_status(&self, cx: &App) -> Option<ThreadStatus> {
501 self.thread_id
502 .map(|id| self.session().read(cx).thread_status(id))
503 }
504
505 fn select_thread(&mut self, thread_id: ThreadId, cx: &mut Context<Self>) {
506 if self.thread_id.is_some_and(|id| id == thread_id) {
507 return;
508 }
509
510 self.thread_id = Some(thread_id);
511
512 self.stack_frame_list
513 .update(cx, |list, cx| list.refresh(cx));
514 cx.notify();
515 }
516
517 pub fn continue_thread(&mut self, cx: &mut Context<Self>) {
518 let Some(thread_id) = self.thread_id else {
519 return;
520 };
521
522 self.session().update(cx, |state, cx| {
523 state.continue_thread(thread_id, cx);
524 });
525 }
526
527 pub fn step_over(&mut self, cx: &mut Context<Self>) {
528 let Some(thread_id) = self.thread_id else {
529 return;
530 };
531
532 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
533
534 self.session().update(cx, |state, cx| {
535 state.step_over(thread_id, granularity, cx);
536 });
537 }
538
539 pub(crate) fn step_in(&mut self, cx: &mut Context<Self>) {
540 let Some(thread_id) = self.thread_id else {
541 return;
542 };
543
544 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
545
546 self.session().update(cx, |state, cx| {
547 state.step_in(thread_id, granularity, cx);
548 });
549 }
550
551 pub(crate) fn step_out(&mut self, cx: &mut Context<Self>) {
552 let Some(thread_id) = self.thread_id else {
553 return;
554 };
555
556 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
557
558 self.session().update(cx, |state, cx| {
559 state.step_out(thread_id, granularity, cx);
560 });
561 }
562
563 pub(crate) fn step_back(&mut self, cx: &mut Context<Self>) {
564 let Some(thread_id) = self.thread_id else {
565 return;
566 };
567
568 let granularity = DebuggerSettings::get_global(cx).stepping_granularity;
569
570 self.session().update(cx, |state, cx| {
571 state.step_back(thread_id, granularity, cx);
572 });
573 }
574
575 pub fn restart_session(&self, cx: &mut Context<Self>) {
576 self.session().update(cx, |state, cx| {
577 state.restart(None, cx);
578 });
579 }
580
581 pub fn pause_thread(&self, cx: &mut Context<Self>) {
582 let Some(thread_id) = self.thread_id else {
583 return;
584 };
585
586 self.session().update(cx, |state, cx| {
587 state.pause_thread(thread_id, cx);
588 });
589 }
590
591 pub(crate) fn shutdown(&mut self, cx: &mut Context<Self>) {
592 self.workspace
593 .update(cx, |workspace, cx| {
594 workspace
595 .project()
596 .read(cx)
597 .breakpoint_store()
598 .update(cx, |store, cx| {
599 store.remove_active_position(Some(self.session_id), cx)
600 })
601 })
602 .log_err();
603
604 self.session.update(cx, |session, cx| {
605 session.shutdown(cx).detach();
606 })
607 }
608
609 pub fn stop_thread(&self, cx: &mut Context<Self>) {
610 let Some(thread_id) = self.thread_id else {
611 return;
612 };
613
614 self.workspace
615 .update(cx, |workspace, cx| {
616 workspace
617 .project()
618 .read(cx)
619 .breakpoint_store()
620 .update(cx, |store, cx| {
621 store.remove_active_position(Some(self.session_id), cx)
622 })
623 })
624 .log_err();
625
626 self.session().update(cx, |state, cx| {
627 state.terminate_threads(Some(vec![thread_id; 1]), cx);
628 });
629 }
630
631 #[expect(
632 unused,
633 reason = "Support for disconnecting a client is not wired through yet"
634 )]
635 pub fn disconnect_client(&self, cx: &mut Context<Self>) {
636 self.session().update(cx, |state, cx| {
637 state.disconnect_client(cx);
638 });
639 }
640
641 pub fn toggle_ignore_breakpoints(&mut self, cx: &mut Context<Self>) {
642 self.session.update(cx, |session, cx| {
643 session.toggle_ignore_breakpoints(cx).detach();
644 });
645 }
646
647 pub(crate) fn thread_dropdown(
648 &self,
649 window: &mut Window,
650 cx: &mut Context<'_, RunningState>,
651 ) -> DropdownMenu {
652 let state = cx.entity();
653 let threads = self.session.update(cx, |this, cx| this.threads(cx));
654 let selected_thread_name = threads
655 .iter()
656 .find(|(thread, _)| self.thread_id.map(|id| id.0) == Some(thread.id))
657 .map(|(thread, _)| thread.name.clone())
658 .unwrap_or("Threads".to_owned());
659 DropdownMenu::new(
660 ("thread-list", self.session_id.0),
661 selected_thread_name,
662 ContextMenu::build(window, cx, move |mut this, _, _| {
663 for (thread, _) in threads {
664 let state = state.clone();
665 let thread_id = thread.id;
666 this = this.entry(thread.name, None, move |_, cx| {
667 state.update(cx, |state, cx| {
668 state.select_thread(ThreadId(thread_id), cx);
669 });
670 });
671 }
672 this
673 }),
674 )
675 }
676}
677
678impl EventEmitter<DebugPanelItemEvent> for RunningState {}
679
680impl Focusable for RunningState {
681 fn focus_handle(&self, _: &App) -> FocusHandle {
682 self.focus_handle.clone()
683 }
684}