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