stack_trace_view.rs

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