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