stack_frame_list.rs

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