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 formatted_path = format!(
325            "{}:{}",
326            source.clone().and_then(|s| s.name).unwrap_or_default(),
327            stack_frame.line,
328        );
329
330        let supports_frame_restart = self
331            .session
332            .read(cx)
333            .capabilities()
334            .supports_restart_frame
335            .unwrap_or_default();
336
337        let origin = stack_frame
338            .source
339            .to_owned()
340            .and_then(|source| source.origin);
341
342        h_flex()
343            .rounded_md()
344            .justify_between()
345            .w_full()
346            .group("")
347            .id(("stack-frame", stack_frame.id))
348            .tooltip({
349                let formatted_path = formatted_path.clone();
350                move |_window, app| {
351                    app.new(|_| {
352                        let mut tooltip = Tooltip::new(formatted_path.clone());
353
354                        if let Some(origin) = &origin {
355                            tooltip = tooltip.meta(origin);
356                        }
357
358                        tooltip
359                    })
360                    .into()
361                }
362            })
363            .p_1()
364            .when(is_selected_frame, |this| {
365                this.bg(cx.theme().colors().element_hover)
366            })
367            .on_click(cx.listener({
368                let stack_frame = stack_frame.clone();
369                move |this, _, window, cx| {
370                    this.select_stack_frame(&stack_frame, true, window, cx)
371                        .detach_and_log_err(cx);
372                }
373            }))
374            .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
375            .child(
376                v_flex()
377                    .child(
378                        h_flex()
379                            .gap_0p5()
380                            .text_ui_sm(cx)
381                            .truncate()
382                            .child(stack_frame.name.clone())
383                            .child(formatted_path),
384                    )
385                    .child(
386                        h_flex()
387                            .text_ui_xs(cx)
388                            .truncate()
389                            .text_color(cx.theme().colors().text_muted)
390                            .when_some(source.and_then(|s| s.path), |this, path| this.child(path)),
391                    ),
392            )
393            .when(
394                supports_frame_restart && stack_frame.can_restart.unwrap_or(true),
395                |this| {
396                    this.child(
397                        h_flex()
398                            .id(("restart-stack-frame", stack_frame.id))
399                            .visible_on_hover("")
400                            .absolute()
401                            .right_2()
402                            .overflow_hidden()
403                            .rounded_md()
404                            .border_1()
405                            .border_color(cx.theme().colors().element_selected)
406                            .bg(cx.theme().colors().element_background)
407                            .hover(|style| {
408                                style
409                                    .bg(cx.theme().colors().ghost_element_hover)
410                                    .cursor_pointer()
411                            })
412                            .child(
413                                IconButton::new(
414                                    ("restart-stack-frame", stack_frame.id),
415                                    IconName::DebugRestart,
416                                )
417                                .icon_size(IconSize::Small)
418                                .on_click(cx.listener({
419                                    let stack_frame_id = stack_frame.id;
420                                    move |this, _, _window, cx| {
421                                        this.restart_stack_frame(stack_frame_id, cx);
422                                    }
423                                }))
424                                .tooltip(move |window, cx| {
425                                    Tooltip::text("Restart Stack Frame")(window, cx)
426                                }),
427                            ),
428                    )
429                },
430            )
431            .into_any()
432    }
433
434    pub fn expand_collapsed_entry(
435        &mut self,
436        ix: usize,
437        stack_frames: &Vec<dap::StackFrame>,
438        cx: &mut Context<Self>,
439    ) {
440        self.entries.splice(
441            ix..ix + 1,
442            stack_frames
443                .iter()
444                .map(|frame| StackFrameEntry::Normal(frame.clone())),
445        );
446        self.list.reset(self.entries.len());
447        cx.notify();
448    }
449
450    fn render_collapsed_entry(
451        &self,
452        ix: usize,
453        stack_frames: &Vec<dap::StackFrame>,
454        cx: &mut Context<Self>,
455    ) -> AnyElement {
456        let first_stack_frame = &stack_frames[0];
457
458        h_flex()
459            .rounded_md()
460            .justify_between()
461            .w_full()
462            .group("")
463            .id(("stack-frame", first_stack_frame.id))
464            .p_1()
465            .on_click(cx.listener({
466                let stack_frames = stack_frames.clone();
467                move |this, _, _window, cx| {
468                    this.expand_collapsed_entry(ix, &stack_frames, cx);
469                }
470            }))
471            .hover(|style| style.bg(cx.theme().colors().element_hover).cursor_pointer())
472            .child(
473                v_flex()
474                    .text_ui_sm(cx)
475                    .truncate()
476                    .text_color(cx.theme().colors().text_muted)
477                    .child(format!(
478                        "Show {} more{}",
479                        stack_frames.len(),
480                        first_stack_frame
481                            .source
482                            .as_ref()
483                            .and_then(|source| source.origin.as_ref())
484                            .map_or(String::new(), |origin| format!(": {}", origin))
485                    )),
486            )
487            .into_any()
488    }
489
490    fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
491        match &self.entries[ix] {
492            StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(stack_frame, cx),
493            StackFrameEntry::Collapsed(stack_frames) => {
494                self.render_collapsed_entry(ix, stack_frames, cx)
495            }
496        }
497    }
498
499    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
500        div()
501            .occlude()
502            .id("stack-frame-list-vertical-scrollbar")
503            .on_mouse_move(cx.listener(|_, _, _, cx| {
504                cx.notify();
505                cx.stop_propagation()
506            }))
507            .on_hover(|_, _, cx| {
508                cx.stop_propagation();
509            })
510            .on_any_mouse_down(|_, _, cx| {
511                cx.stop_propagation();
512            })
513            .on_mouse_up(
514                MouseButton::Left,
515                cx.listener(|_, _, _, cx| {
516                    cx.stop_propagation();
517                }),
518            )
519            .on_scroll_wheel(cx.listener(|_, _, _, cx| {
520                cx.notify();
521            }))
522            .h_full()
523            .absolute()
524            .right_1()
525            .top_1()
526            .bottom_0()
527            .w(px(12.))
528            .cursor_default()
529            .children(Scrollbar::vertical(self.scrollbar_state.clone()))
530    }
531}
532
533impl Render for StackFrameList {
534    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
535        if self.invalidate {
536            self.build_entries(self.entries.is_empty(), window, cx);
537            self.invalidate = false;
538            cx.notify();
539        }
540
541        div()
542            .size_full()
543            .p_1()
544            .child(list(self.list.clone()).size_full())
545            .child(self.render_vertical_scrollbar(cx))
546    }
547}
548
549impl Focusable for StackFrameList {
550    fn focus_handle(&self, _: &gpui::App) -> gpui::FocusHandle {
551        self.focus_handle.clone()
552    }
553}
554
555impl EventEmitter<StackFrameListEvent> for StackFrameList {}