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