stack_trace_view.rs

  1use std::any::{Any, TypeId};
  2
  3use collections::HashMap;
  4use dap::StackFrameId;
  5use editor::{
  6    Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
  7    RowHighlightOptions, SelectionEffects, ToPoint, scroll::Autoscroll,
  8};
  9use gpui::{
 10    AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
 11    Subscription, Task, WeakEntity, Window,
 12};
 13use language::{BufferSnapshot, Capability, Point, Selection, SelectionGoal, TreeSitterOptions};
 14use project::{Project, ProjectPath};
 15use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div};
 16use util::ResultExt as _;
 17use workspace::{
 18    Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
 19    item::{BreadcrumbText, ItemEvent, SaveOptions},
 20    searchable::SearchableItemHandle,
 21};
 22
 23use crate::session::running::stack_frame_list::{StackFrameList, StackFrameListEvent};
 24use anyhow::Result;
 25
 26pub(crate) struct StackTraceView {
 27    editor: Entity<Editor>,
 28    multibuffer: Entity<MultiBuffer>,
 29    workspace: WeakEntity<Workspace>,
 30    project: Entity<Project>,
 31    stack_frame_list: Entity<StackFrameList>,
 32    selected_stack_frame_id: Option<StackFrameId>,
 33    highlights: Vec<(StackFrameId, Anchor)>,
 34    excerpt_for_frames: collections::HashMap<ExcerptId, StackFrameId>,
 35    refresh_task: Option<Task<Result<()>>>,
 36    _subscription: Option<Subscription>,
 37}
 38
 39impl StackTraceView {
 40    pub(crate) fn new(
 41        workspace: WeakEntity<Workspace>,
 42        project: Entity<Project>,
 43        stack_frame_list: Entity<StackFrameList>,
 44        window: &mut Window,
 45        cx: &mut Context<Self>,
 46    ) -> Self {
 47        let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
 48        let editor = cx.new(|cx| {
 49            let mut editor =
 50                Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
 51            editor.set_vertical_scroll_margin(5, cx);
 52            editor
 53        });
 54
 55        cx.subscribe_in(&editor, window, |this, editor, event, window, cx| {
 56            if let EditorEvent::SelectionsChanged { local: true } = event {
 57                let excerpt_id = editor.update(cx, |editor, cx| {
 58                    let position: Point = editor.selections.newest(cx).head();
 59
 60                    editor
 61                        .snapshot(window, cx)
 62                        .buffer_snapshot()
 63                        .excerpt_containing(position..position)
 64                        .map(|excerpt| excerpt.id())
 65                });
 66
 67                if let Some(stack_frame_id) = excerpt_id
 68                    .and_then(|id| this.excerpt_for_frames.get(&id))
 69                    .filter(|id| Some(**id) != this.selected_stack_frame_id)
 70                {
 71                    this.stack_frame_list.update(cx, |list, cx| {
 72                        list.go_to_stack_frame(*stack_frame_id, window, cx).detach();
 73                    });
 74                }
 75            }
 76        })
 77        .detach();
 78
 79        cx.subscribe_in(
 80            &stack_frame_list,
 81            window,
 82            |this, stack_frame_list, event, window, cx| match event {
 83                StackFrameListEvent::BuiltEntries => {
 84                    this.selected_stack_frame_id =
 85                        stack_frame_list.read(cx).opened_stack_frame_id();
 86                    this.update_excerpts(window, cx);
 87                }
 88                StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => {
 89                    this.selected_stack_frame_id = Some(*selected_frame_id);
 90                    this.update_highlights(window, cx);
 91
 92                    if let Some(frame_anchor) = this
 93                        .highlights
 94                        .iter()
 95                        .find(|(frame_id, _)| frame_id == selected_frame_id)
 96                        .map(|highlight| highlight.1)
 97                    {
 98                        this.editor.update(cx, |editor, cx| {
 99                            if frame_anchor.excerpt_id
100                                != editor.selections.newest_anchor().head().excerpt_id
101                            {
102                                let effects = SelectionEffects::scroll(
103                                    Autoscroll::center().for_anchor(frame_anchor),
104                                );
105
106                                editor.change_selections(effects, window, cx, |selections| {
107                                    let selection_id = selections.new_selection_id();
108
109                                    let selection = Selection {
110                                        id: selection_id,
111                                        start: frame_anchor,
112                                        end: frame_anchor,
113                                        goal: SelectionGoal::None,
114                                        reversed: false,
115                                    };
116
117                                    selections.select_anchors(vec![selection]);
118                                })
119                            }
120                        });
121                    }
122                }
123            },
124        )
125        .detach();
126
127        let mut this = Self {
128            editor,
129            multibuffer,
130            workspace,
131            project,
132            excerpt_for_frames: HashMap::default(),
133            highlights: Vec::default(),
134            stack_frame_list,
135            selected_stack_frame_id: None,
136            refresh_task: None,
137            _subscription: None,
138        };
139
140        this.update_excerpts(window, cx);
141        this
142    }
143
144    fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
145        self.refresh_task.take();
146        self.editor.update(cx, |editor, cx| {
147            editor.clear_highlights::<DebugStackFrameLine>(cx)
148        });
149
150        let stack_frames = self
151            .stack_frame_list
152            .read_with(cx, |list, _| list.flatten_entries(false, false));
153
154        let frames_to_open: Vec<_> = stack_frames
155            .into_iter()
156            .filter_map(|frame| {
157                Some((
158                    frame.id,
159                    frame.line as u32 - 1,
160                    StackFrameList::abs_path_from_stack_frame(&frame)?,
161                ))
162            })
163            .collect();
164
165        self.multibuffer
166            .update(cx, |multi_buffer, cx| multi_buffer.clear(cx));
167
168        let task = cx.spawn_in(window, async move |this, cx| {
169            let mut to_highlights = Vec::default();
170
171            for (stack_frame_id, line, abs_path) in frames_to_open {
172                let (worktree, relative_path) = this
173                    .update(cx, |this, cx| {
174                        this.workspace.update(cx, |workspace, cx| {
175                            workspace.project().update(cx, |this, cx| {
176                                this.find_or_create_worktree(&abs_path, false, cx)
177                            })
178                        })
179                    })??
180                    .await?;
181
182                let project_path = ProjectPath {
183                    worktree_id: worktree.read_with(cx, |tree, _| tree.id())?,
184                    path: relative_path,
185                };
186
187                if let Some(buffer) = this
188                    .read_with(cx, |this, _| this.project.clone())?
189                    .update(cx, |project, cx| project.open_buffer(project_path, cx))?
190                    .await
191                    .log_err()
192                {
193                    this.update(cx, |this, cx| {
194                        this.multibuffer.update(cx, |multi_buffer, cx| {
195                            let line_point = Point::new(line, 0);
196                            let start_context = Self::heuristic_syntactic_expand(
197                                &buffer.read(cx).snapshot(),
198                                line_point,
199                            );
200
201                            // Users will want to see what happened before an active debug line in most cases
202                            let range = ExcerptRange {
203                                context: start_context..Point::new(line.saturating_add(1), 0),
204                                primary: line_point..line_point,
205                            };
206                            multi_buffer.push_excerpts(buffer.clone(), vec![range], cx);
207
208                            let line_anchor =
209                                multi_buffer.buffer_point_to_anchor(&buffer, line_point, cx);
210
211                            if let Some(line_anchor) = line_anchor {
212                                this.excerpt_for_frames
213                                    .insert(line_anchor.excerpt_id, stack_frame_id);
214                                to_highlights.push((stack_frame_id, line_anchor));
215                            }
216                        });
217                    })
218                    .ok();
219                }
220            }
221
222            this.update_in(cx, |this, window, cx| {
223                this.highlights = to_highlights;
224                this.update_highlights(window, cx);
225            })
226            .ok();
227
228            anyhow::Ok(())
229        });
230
231        self.refresh_task = Some(task);
232    }
233
234    fn update_highlights(&mut self, window: &mut Window, cx: &mut Context<Self>) {
235        self.editor.update(cx, |editor, _| {
236            editor.clear_row_highlights::<DebugStackFrameLine>()
237        });
238
239        let stack_frames = self
240            .stack_frame_list
241            .read_with(cx, |session, _| session.flatten_entries(false, false));
242
243        let active_idx = self
244            .selected_stack_frame_id
245            .and_then(|id| {
246                stack_frames
247                    .iter()
248                    .enumerate()
249                    .find_map(|(idx, frame)| if frame.id == id { Some(idx) } else { None })
250            })
251            .unwrap_or(0);
252
253        self.editor.update(cx, |editor, cx| {
254            let snapshot = editor.snapshot(window, cx).display_snapshot;
255            let first_color = cx.theme().colors().editor_debugger_active_line_background;
256
257            let color = first_color.opacity(0.5);
258
259            let mut is_first = true;
260
261            for (_, highlight) in self.highlights.iter().skip(active_idx) {
262                let position = highlight.to_point(&snapshot.buffer_snapshot());
263                let color = if is_first {
264                    is_first = false;
265                    first_color
266                } else {
267                    color
268                };
269
270                let start = snapshot
271                    .buffer_snapshot()
272                    .clip_point(Point::new(position.row, 0), Bias::Left);
273                let end = start + Point::new(1, 0);
274                let start = snapshot.buffer_snapshot().anchor_before(start);
275                let end = snapshot.buffer_snapshot().anchor_before(end);
276                editor.highlight_rows::<DebugStackFrameLine>(
277                    start..end,
278                    color,
279                    RowHighlightOptions::default(),
280                    cx,
281                );
282            }
283        })
284    }
285
286    fn heuristic_syntactic_expand(snapshot: &BufferSnapshot, selected_point: Point) -> Point {
287        let mut text_objects = snapshot.text_object_ranges(
288            selected_point..selected_point,
289            TreeSitterOptions::max_start_depth(4),
290        );
291
292        let mut start_position = text_objects
293            .find(|(_, obj)| matches!(obj, language::TextObject::AroundFunction))
294            .map(|(range, _)| snapshot.offset_to_point(range.start))
295            .map(|point| Point::new(point.row.max(selected_point.row.saturating_sub(8)), 0))
296            .unwrap_or(selected_point);
297
298        if start_position.row == selected_point.row {
299            start_position.row = start_position.row.saturating_sub(1);
300        }
301
302        start_position
303    }
304}
305
306impl Render for StackTraceView {
307    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
308        div().size_full().child(self.editor.clone())
309    }
310}
311
312impl EventEmitter<EditorEvent> for StackTraceView {}
313impl Focusable for StackTraceView {
314    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
315        self.editor.focus_handle(cx)
316    }
317}
318
319impl Item for StackTraceView {
320    type Event = EditorEvent;
321
322    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
323        Editor::to_item_events(event, f)
324    }
325
326    fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
327        self.editor
328            .update(cx, |editor, cx| editor.deactivated(window, cx));
329    }
330
331    fn navigate(
332        &mut self,
333        data: Box<dyn Any>,
334        window: &mut Window,
335        cx: &mut Context<Self>,
336    ) -> bool {
337        self.editor
338            .update(cx, |editor, cx| editor.navigate(data, window, cx))
339    }
340
341    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
342        Some("Stack Frame Viewer".into())
343    }
344
345    fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
346        "Stack Frames".into()
347    }
348
349    fn for_each_project_item(
350        &self,
351        cx: &App,
352        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
353    ) {
354        self.editor.for_each_project_item(cx, f)
355    }
356
357    fn set_nav_history(
358        &mut self,
359        nav_history: ItemNavHistory,
360        _: &mut Window,
361        cx: &mut Context<Self>,
362    ) {
363        self.editor.update(cx, |editor, _| {
364            editor.set_nav_history(Some(nav_history));
365        });
366    }
367
368    fn is_dirty(&self, cx: &App) -> bool {
369        self.multibuffer.read(cx).is_dirty(cx)
370    }
371
372    fn has_deleted_file(&self, cx: &App) -> bool {
373        self.multibuffer.read(cx).has_deleted_file(cx)
374    }
375
376    fn has_conflict(&self, cx: &App) -> bool {
377        self.multibuffer.read(cx).has_conflict(cx)
378    }
379
380    fn can_save(&self, _: &App) -> bool {
381        true
382    }
383
384    fn save(
385        &mut self,
386        options: SaveOptions,
387        project: Entity<Project>,
388        window: &mut Window,
389        cx: &mut Context<Self>,
390    ) -> Task<Result<()>> {
391        self.editor.save(options, project, window, cx)
392    }
393
394    fn save_as(
395        &mut self,
396        _: Entity<Project>,
397        _: ProjectPath,
398        _window: &mut Window,
399        _: &mut Context<Self>,
400    ) -> Task<Result<()>> {
401        unreachable!()
402    }
403
404    fn reload(
405        &mut self,
406        project: Entity<Project>,
407        window: &mut Window,
408        cx: &mut Context<Self>,
409    ) -> Task<Result<()>> {
410        self.editor.reload(project, window, cx)
411    }
412
413    fn act_as_type<'a>(
414        &'a self,
415        type_id: TypeId,
416        self_handle: &'a Entity<Self>,
417        _: &'a App,
418    ) -> Option<AnyView> {
419        if type_id == TypeId::of::<Self>() {
420            Some(self_handle.to_any())
421        } else if type_id == TypeId::of::<Editor>() {
422            Some(self.editor.to_any())
423        } else {
424            None
425        }
426    }
427
428    fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
429        Some(Box::new(self.editor.clone()))
430    }
431
432    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
433        ToolbarItemLocation::PrimaryLeft
434    }
435
436    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
437        self.editor.breadcrumbs(theme, cx)
438    }
439
440    fn added_to_workspace(
441        &mut self,
442        workspace: &mut Workspace,
443        window: &mut Window,
444        cx: &mut Context<Self>,
445    ) {
446        self.editor.update(cx, |editor, cx| {
447            editor.added_to_workspace(workspace, window, cx)
448        });
449    }
450}