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