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, 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 {}