stack_frame_list.rs

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