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