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