stack_frame_list.rs

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