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