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