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