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