1use std::path::Path;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Context as _, Result, anyhow};
6use dap::StackFrameId;
7use gpui::{
8 AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton,
9 Stateful, Subscription, Task, WeakEntity, list,
10};
11use util::debug_panic;
12
13use crate::StackTraceView;
14use language::PointUtf16;
15use project::debugger::breakpoint_store::ActiveStackFrame;
16use project::debugger::session::{Session, SessionEvent, StackFrame};
17use project::{ProjectItem, ProjectPath};
18use ui::{Scrollbar, ScrollbarState, Tooltip, prelude::*};
19use workspace::{ItemHandle, Workspace};
20
21use super::RunningState;
22
23#[derive(Debug)]
24pub enum StackFrameListEvent {
25 SelectedStackFrameChanged(StackFrameId),
26 BuiltEntries,
27}
28
29pub struct StackFrameList {
30 focus_handle: FocusHandle,
31 _subscription: Subscription,
32 session: Entity<Session>,
33 state: WeakEntity<RunningState>,
34 entries: Vec<StackFrameEntry>,
35 workspace: WeakEntity<Workspace>,
36 selected_ix: Option<usize>,
37 opened_stack_frame_id: Option<StackFrameId>,
38 scrollbar_state: ScrollbarState,
39 list_state: ListState,
40 error: Option<SharedString>,
41 _refresh_task: Task<()>,
42}
43
44#[derive(Debug, PartialEq, Eq)]
45pub enum StackFrameEntry {
46 Normal(dap::StackFrame),
47 /// Used to indicate that the frame is artificial and is a visual label or separator
48 Label(dap::StackFrame),
49 Collapsed(Vec<dap::StackFrame>),
50}
51
52impl StackFrameList {
53 pub fn new(
54 workspace: WeakEntity<Workspace>,
55 session: Entity<Session>,
56 state: WeakEntity<RunningState>,
57 window: &mut Window,
58 cx: &mut Context<Self>,
59 ) -> Self {
60 let focus_handle = cx.focus_handle();
61
62 let _subscription =
63 cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
64 SessionEvent::Threads => {
65 this.schedule_refresh(false, window, cx);
66 }
67 SessionEvent::Stopped(..) | SessionEvent::StackTrace => {
68 this.schedule_refresh(true, window, cx);
69 }
70 _ => {}
71 });
72
73 let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.));
74 let scrollbar_state = ScrollbarState::new(list_state.clone());
75
76 let mut this = Self {
77 session,
78 workspace,
79 focus_handle,
80 state,
81 _subscription,
82 entries: Default::default(),
83 error: None,
84 selected_ix: None,
85 opened_stack_frame_id: None,
86 list_state,
87 scrollbar_state,
88 _refresh_task: Task::ready(()),
89 };
90 this.schedule_refresh(true, window, cx);
91 this
92 }
93
94 #[cfg(test)]
95 pub(crate) fn entries(&self) -> &Vec<StackFrameEntry> {
96 &self.entries
97 }
98
99 pub(crate) fn flatten_entries(
100 &self,
101 show_collapsed: bool,
102 show_labels: bool,
103 ) -> Vec<dap::StackFrame> {
104 self.entries
105 .iter()
106 .flat_map(|frame| match frame {
107 StackFrameEntry::Normal(frame) => vec![frame.clone()],
108 StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
109 StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
110 _ => vec![],
111 })
112 .collect::<Vec<_>>()
113 }
114
115 fn stack_frames(&self, cx: &mut App) -> Result<Vec<StackFrame>> {
116 if let Ok(Some(thread_id)) = self.state.read_with(cx, |state, _| state.thread_id) {
117 self.session
118 .update(cx, |this, cx| this.stack_frames(thread_id, cx))
119 } else {
120 Ok(Vec::default())
121 }
122 }
123
124 #[cfg(test)]
125 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
126 self.stack_frames(cx)
127 .unwrap_or_default()
128 .into_iter()
129 .map(|stack_frame| stack_frame.dap)
130 .collect()
131 }
132
133 pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
134 self.opened_stack_frame_id
135 }
136
137 pub(super) fn schedule_refresh(
138 &mut self,
139 select_first: bool,
140 window: &mut Window,
141 cx: &mut Context<Self>,
142 ) {
143 const REFRESH_DEBOUNCE: Duration = Duration::from_millis(20);
144
145 self._refresh_task = cx.spawn_in(window, async move |this, cx| {
146 let debounce = this
147 .update(cx, |this, cx| {
148 let new_stack_frames = this.stack_frames(cx);
149 new_stack_frames.unwrap_or_default().is_empty() && !this.entries.is_empty()
150 })
151 .ok()
152 .unwrap_or_default();
153
154 if debounce {
155 cx.background_executor().timer(REFRESH_DEBOUNCE).await;
156 }
157 this.update_in(cx, |this, window, cx| {
158 this.build_entries(select_first, window, cx);
159 cx.notify();
160 })
161 .ok();
162 })
163 }
164
165 pub fn build_entries(
166 &mut self,
167 open_first_stack_frame: bool,
168 window: &mut Window,
169 cx: &mut Context<Self>,
170 ) {
171 let old_selected_frame_id = self
172 .selected_ix
173 .and_then(|ix| self.entries.get(ix))
174 .and_then(|entry| match entry {
175 StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
176 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None,
177 });
178 let mut entries = Vec::new();
179 let mut collapsed_entries = Vec::new();
180 let mut first_stack_frame = None;
181 let mut first_stack_frame_with_path = None;
182
183 let stack_frames = match self.stack_frames(cx) {
184 Ok(stack_frames) => stack_frames,
185 Err(e) => {
186 self.error = Some(format!("{}", e).into());
187 self.entries.clear();
188 self.selected_ix = None;
189 self.list_state.reset(0);
190 cx.emit(StackFrameListEvent::BuiltEntries);
191 cx.notify();
192 return;
193 }
194 };
195 for stack_frame in &stack_frames {
196 match stack_frame.dap.presentation_hint {
197 Some(dap::StackFramePresentationHint::Deemphasize)
198 | Some(dap::StackFramePresentationHint::Subtle) => {
199 collapsed_entries.push(stack_frame.dap.clone());
200 }
201 Some(dap::StackFramePresentationHint::Label) => {
202 entries.push(StackFrameEntry::Label(stack_frame.dap.clone()));
203 }
204 _ => {
205 let collapsed_entries = std::mem::take(&mut collapsed_entries);
206 if !collapsed_entries.is_empty() {
207 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
208 }
209
210 first_stack_frame.get_or_insert(entries.len());
211
212 if stack_frame
213 .dap
214 .source
215 .as_ref()
216 .is_some_and(|source| source.path.is_some())
217 {
218 first_stack_frame_with_path.get_or_insert(entries.len());
219 }
220 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
221 }
222 }
223 }
224
225 let collapsed_entries = std::mem::take(&mut collapsed_entries);
226 if !collapsed_entries.is_empty() {
227 entries.push(StackFrameEntry::Collapsed(collapsed_entries));
228 }
229 self.entries = entries;
230
231 if let Some(ix) = first_stack_frame_with_path
232 .or(first_stack_frame)
233 .filter(|_| open_first_stack_frame)
234 {
235 self.select_ix(Some(ix), cx);
236 self.activate_selected_entry(window, cx);
237 } else if let Some(old_selected_frame_id) = old_selected_frame_id {
238 let ix = self.entries.iter().position(|entry| match entry {
239 StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
240 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false,
241 });
242 self.selected_ix = ix;
243 }
244
245 self.list_state.reset(self.entries.len());
246 cx.emit(StackFrameListEvent::BuiltEntries);
247 cx.notify();
248 }
249
250 pub fn go_to_stack_frame(
251 &mut self,
252 stack_frame_id: StackFrameId,
253 window: &mut Window,
254 cx: &mut Context<Self>,
255 ) -> Task<Result<()>> {
256 let Some(stack_frame) = self
257 .entries
258 .iter()
259 .flat_map(|entry| match entry {
260 StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame),
261 StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
262 StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
263 })
264 .find(|stack_frame| stack_frame.id == stack_frame_id)
265 .cloned()
266 else {
267 return Task::ready(Err(anyhow!("No stack frame for ID")));
268 };
269 self.go_to_stack_frame_inner(stack_frame, window, cx)
270 }
271
272 fn go_to_stack_frame_inner(
273 &mut self,
274 stack_frame: dap::StackFrame,
275 window: &mut Window,
276 cx: &mut Context<Self>,
277 ) -> Task<Result<()>> {
278 let stack_frame_id = stack_frame.id;
279 self.opened_stack_frame_id = Some(stack_frame_id);
280 let Some(abs_path) = Self::abs_path_from_stack_frame(&stack_frame) else {
281 return Task::ready(Err(anyhow!("Project path not found")));
282 };
283 let row = stack_frame.line.saturating_sub(1) as u32;
284 cx.emit(StackFrameListEvent::SelectedStackFrameChanged(
285 stack_frame_id,
286 ));
287 cx.spawn_in(window, async move |this, cx| {
288 let (worktree, relative_path) = this
289 .update(cx, |this, cx| {
290 this.workspace.update(cx, |workspace, cx| {
291 workspace.project().update(cx, |this, cx| {
292 this.find_or_create_worktree(&abs_path, false, cx)
293 })
294 })
295 })??
296 .await?;
297 let buffer = this
298 .update(cx, |this, cx| {
299 this.workspace.update(cx, |this, cx| {
300 this.project().update(cx, |this, cx| {
301 let worktree_id = worktree.read(cx).id();
302 this.open_buffer(
303 ProjectPath {
304 worktree_id,
305 path: relative_path.into(),
306 },
307 cx,
308 )
309 })
310 })
311 })??
312 .await?;
313 let position = buffer.read_with(cx, |this, _| {
314 this.snapshot().anchor_after(PointUtf16::new(row, 0))
315 })?;
316 this.update_in(cx, |this, window, cx| {
317 this.workspace.update(cx, |workspace, cx| {
318 let project_path = buffer
319 .read(cx)
320 .project_path(cx)
321 .context("Could not select a stack frame for unnamed buffer")?;
322
323 let open_preview = !workspace
324 .item_of_type::<StackTraceView>(cx)
325 .map(|viewer| {
326 workspace
327 .active_item(cx)
328 .is_some_and(|item| item.item_id() == viewer.item_id())
329 })
330 .unwrap_or_default();
331
332 anyhow::Ok(workspace.open_path_preview(
333 project_path,
334 None,
335 true,
336 true,
337 open_preview,
338 window,
339 cx,
340 ))
341 })
342 })???
343 .await?;
344
345 this.update(cx, |this, cx| {
346 let thread_id = this.state.read_with(cx, |state, _| {
347 state.thread_id.context("No selected thread ID found")
348 })??;
349
350 this.workspace.update(cx, |workspace, cx| {
351 let breakpoint_store = workspace.project().read(cx).breakpoint_store();
352
353 breakpoint_store.update(cx, |store, cx| {
354 store.set_active_position(
355 ActiveStackFrame {
356 session_id: this.session.read(cx).session_id(),
357 thread_id,
358 stack_frame_id,
359 path: abs_path,
360 position,
361 },
362 cx,
363 );
364 })
365 })
366 })?
367 })
368 }
369
370 pub(crate) fn abs_path_from_stack_frame(stack_frame: &dap::StackFrame) -> Option<Arc<Path>> {
371 stack_frame.source.as_ref().and_then(|s| {
372 s.path
373 .as_deref()
374 .map(|path| Arc::<Path>::from(Path::new(path)))
375 .filter(|path| path.is_absolute())
376 })
377 }
378
379 pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context<Self>) {
380 self.session.update(cx, |state, cx| {
381 state.restart_stack_frame(stack_frame_id, cx)
382 });
383 }
384
385 fn render_label_entry(
386 &self,
387 stack_frame: &dap::StackFrame,
388 _cx: &mut Context<Self>,
389 ) -> AnyElement {
390 h_flex()
391 .rounded_md()
392 .justify_between()
393 .w_full()
394 .group("")
395 .id(("label-stack-frame", stack_frame.id))
396 .p_1()
397 .on_any_mouse_down(|_, _, cx| {
398 cx.stop_propagation();
399 })
400 .child(
401 v_flex().justify_center().gap_0p5().child(
402 Label::new(stack_frame.name.clone())
403 .size(LabelSize::Small)
404 .weight(FontWeight::BOLD)
405 .truncate()
406 .color(Color::Info),
407 ),
408 )
409 .into_any()
410 }
411
412 fn render_normal_entry(
413 &self,
414 ix: usize,
415 stack_frame: &dap::StackFrame,
416 cx: &mut Context<Self>,
417 ) -> AnyElement {
418 let source = stack_frame.source.clone();
419 let is_selected_frame = Some(ix) == self.selected_ix;
420
421 let path = source.and_then(|s| s.path.or(s.name));
422 let formatted_path = path.map(|path| format!("{}:{}", path, stack_frame.line,));
423 let formatted_path = formatted_path.map(|path| {
424 Label::new(path)
425 .size(LabelSize::XSmall)
426 .line_height_style(LineHeightStyle::UiLabel)
427 .truncate()
428 .color(Color::Muted)
429 });
430
431 let supports_frame_restart = self
432 .session
433 .read(cx)
434 .capabilities()
435 .supports_restart_frame
436 .unwrap_or_default();
437
438 let should_deemphasize = matches!(
439 stack_frame.presentation_hint,
440 Some(
441 dap::StackFramePresentationHint::Subtle
442 | dap::StackFramePresentationHint::Deemphasize
443 )
444 );
445 h_flex()
446 .rounded_md()
447 .justify_between()
448 .w_full()
449 .group("")
450 .id(("stack-frame", stack_frame.id))
451 .p_1()
452 .when(is_selected_frame, |this| {
453 this.bg(cx.theme().colors().element_hover)
454 })
455 .on_any_mouse_down(|_, _, cx| {
456 cx.stop_propagation();
457 })
458 .on_click(cx.listener(move |this, _, window, cx| {
459 this.selected_ix = Some(ix);
460 this.activate_selected_entry(window, cx);
461 }))
462 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
463 .child(
464 v_flex()
465 .gap_0p5()
466 .child(
467 Label::new(stack_frame.name.clone())
468 .size(LabelSize::Small)
469 .truncate()
470 .when(should_deemphasize, |this| this.color(Color::Muted)),
471 )
472 .children(formatted_path),
473 )
474 .when(
475 supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
476 |this| {
477 this.child(
478 h_flex()
479 .id(("restart-stack-frame", stack_frame.id))
480 .visible_on_hover("")
481 .absolute()
482 .right_2()
483 .overflow_hidden()
484 .rounded_md()
485 .border_1()
486 .border_color(cx.theme().colors().element_selected)
487 .bg(cx.theme().colors().element_background)
488 .hover(|style| {
489 style
490 .bg(cx.theme().colors().ghost_element_hover)
491 .cursor_pointer()
492 })
493 .child(
494 IconButton::new(
495 ("restart-stack-frame", stack_frame.id),
496 IconName::RotateCcw,
497 )
498 .icon_size(IconSize::Small)
499 .on_click(cx.listener({
500 let stack_frame_id = stack_frame.id;
501 move |this, _, _window, cx| {
502 this.restart_stack_frame(stack_frame_id, cx);
503 }
504 }))
505 .tooltip(move |window, cx| {
506 Tooltip::text("Restart Stack Frame")(window, cx)
507 }),
508 ),
509 )
510 },
511 )
512 .into_any()
513 }
514
515 pub(crate) fn expand_collapsed_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
516 let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
517 return;
518 };
519 let entries = std::mem::take(stack_frames)
520 .into_iter()
521 .map(StackFrameEntry::Normal);
522 self.entries.splice(ix..ix + 1, entries);
523 self.selected_ix = Some(ix);
524 self.list_state.reset(self.entries.len());
525 cx.emit(StackFrameListEvent::BuiltEntries);
526 cx.notify();
527 }
528
529 fn render_collapsed_entry(
530 &self,
531 ix: usize,
532 stack_frames: &Vec<dap::StackFrame>,
533 cx: &mut Context<Self>,
534 ) -> AnyElement {
535 let first_stack_frame = &stack_frames[0];
536 let is_selected = Some(ix) == self.selected_ix;
537
538 h_flex()
539 .rounded_md()
540 .justify_between()
541 .w_full()
542 .group("")
543 .id(("stack-frame", first_stack_frame.id))
544 .p_1()
545 .when(is_selected, |this| {
546 this.bg(cx.theme().colors().element_hover)
547 })
548 .on_any_mouse_down(|_, _, cx| {
549 cx.stop_propagation();
550 })
551 .on_click(cx.listener(move |this, _, window, cx| {
552 this.selected_ix = Some(ix);
553 this.activate_selected_entry(window, cx);
554 }))
555 .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
556 .child(
557 v_flex()
558 .text_ui_sm(cx)
559 .truncate()
560 .text_color(cx.theme().colors().text_muted)
561 .child(format!(
562 "Show {} more{}",
563 stack_frames.len(),
564 first_stack_frame
565 .source
566 .as_ref()
567 .and_then(|source| source.origin.as_ref())
568 .map_or(String::new(), |origin| format!(": {}", origin))
569 )),
570 )
571 .into_any()
572 }
573
574 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
575 match &self.entries[ix] {
576 StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
577 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
578 StackFrameEntry::Collapsed(stack_frames) => {
579 self.render_collapsed_entry(ix, stack_frames, cx)
580 }
581 }
582 }
583
584 fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
585 div()
586 .occlude()
587 .id("stack-frame-list-vertical-scrollbar")
588 .on_mouse_move(cx.listener(|_, _, _, cx| {
589 cx.notify();
590 cx.stop_propagation()
591 }))
592 .on_hover(|_, _, cx| {
593 cx.stop_propagation();
594 })
595 .on_any_mouse_down(|_, _, cx| {
596 cx.stop_propagation();
597 })
598 .on_mouse_up(
599 MouseButton::Left,
600 cx.listener(|_, _, _, cx| {
601 cx.stop_propagation();
602 }),
603 )
604 .on_scroll_wheel(cx.listener(|_, _, _, cx| {
605 cx.notify();
606 }))
607 .h_full()
608 .absolute()
609 .right_1()
610 .top_1()
611 .bottom_0()
612 .w(px(12.))
613 .cursor_default()
614 .children(Scrollbar::vertical(self.scrollbar_state.clone()))
615 }
616
617 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
618 self.selected_ix = ix;
619 cx.notify();
620 }
621
622 fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
623 let ix = match self.selected_ix {
624 _ if self.entries.is_empty() => None,
625 None => Some(0),
626 Some(ix) => {
627 if ix == self.entries.len() - 1 {
628 Some(0)
629 } else {
630 Some(ix + 1)
631 }
632 }
633 };
634 self.select_ix(ix, cx);
635 }
636
637 fn select_previous(
638 &mut self,
639 _: &menu::SelectPrevious,
640 _window: &mut Window,
641 cx: &mut Context<Self>,
642 ) {
643 let ix = match self.selected_ix {
644 _ if self.entries.is_empty() => None,
645 None => Some(self.entries.len() - 1),
646 Some(ix) => {
647 if ix == 0 {
648 Some(self.entries.len() - 1)
649 } else {
650 Some(ix - 1)
651 }
652 }
653 };
654 self.select_ix(ix, cx);
655 }
656
657 fn select_first(
658 &mut self,
659 _: &menu::SelectFirst,
660 _window: &mut Window,
661 cx: &mut Context<Self>,
662 ) {
663 let ix = if !self.entries.is_empty() {
664 Some(0)
665 } else {
666 None
667 };
668 self.select_ix(ix, cx);
669 }
670
671 fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
672 let ix = if !self.entries.is_empty() {
673 Some(self.entries.len() - 1)
674 } else {
675 None
676 };
677 self.select_ix(ix, cx);
678 }
679
680 fn activate_selected_entry(&mut self, window: &mut Window, cx: &mut Context<Self>) {
681 let Some(ix) = self.selected_ix else {
682 return;
683 };
684 let Some(entry) = self.entries.get_mut(ix) else {
685 return;
686 };
687 match entry {
688 StackFrameEntry::Normal(stack_frame) => {
689 let stack_frame = stack_frame.clone();
690 self.go_to_stack_frame_inner(stack_frame, window, cx)
691 .detach_and_log_err(cx)
692 }
693 StackFrameEntry::Label(_) => {
694 debug_panic!("You should not be able to select a label stack frame")
695 }
696 StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix, cx),
697 }
698 cx.notify();
699 }
700
701 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
702 self.activate_selected_entry(window, cx);
703 }
704
705 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
706 div().p_1().size_full().child(
707 list(
708 self.list_state.clone(),
709 cx.processor(|this, ix, _window, cx| this.render_entry(ix, cx)),
710 )
711 .size_full(),
712 )
713 }
714}
715
716impl Render for StackFrameList {
717 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
718 div()
719 .track_focus(&self.focus_handle)
720 .size_full()
721 .on_action(cx.listener(Self::select_next))
722 .on_action(cx.listener(Self::select_previous))
723 .on_action(cx.listener(Self::select_first))
724 .on_action(cx.listener(Self::select_last))
725 .on_action(cx.listener(Self::confirm))
726 .when_some(self.error.clone(), |el, error| {
727 el.child(
728 h_flex()
729 .bg(cx.theme().status().warning_background)
730 .border_b_1()
731 .border_color(cx.theme().status().warning_border)
732 .pl_1()
733 .child(Icon::new(IconName::Warning).color(Color::Warning))
734 .gap_2()
735 .child(
736 Label::new(error)
737 .size(LabelSize::Small)
738 .color(Color::Warning),
739 ),
740 )
741 })
742 .child(self.render_list(window, cx))
743 .child(self.render_vertical_scrollbar(cx))
744 }
745}
746
747impl Focusable for StackFrameList {
748 fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
749 self.focus_handle.clone()
750 }
751}
752
753impl EventEmitter<StackFrameListEvent> for StackFrameList {}