stack_trace_view.rs

  1use std::{
  2    any::{Any, TypeId},
  3    sync::Arc,
  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    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(
338        &mut self,
339        data: Arc<dyn Any + Send>,
340        window: &mut Window,
341        cx: &mut Context<Self>,
342    ) -> bool {
343        self.editor
344            .update(cx, |editor, cx| editor.navigate(data, window, cx))
345    }
346
347    fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
348        Some("Stack Frame Viewer".into())
349    }
350
351    fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString {
352        "Stack Frames".into()
353    }
354
355    fn for_each_project_item(
356        &self,
357        cx: &App,
358        f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
359    ) {
360        self.editor.for_each_project_item(cx, f)
361    }
362
363    fn set_nav_history(
364        &mut self,
365        nav_history: ItemNavHistory,
366        _: &mut Window,
367        cx: &mut Context<Self>,
368    ) {
369        self.editor.update(cx, |editor, _| {
370            editor.set_nav_history(Some(nav_history));
371        });
372    }
373
374    fn is_dirty(&self, cx: &App) -> bool {
375        self.multibuffer.read(cx).is_dirty(cx)
376    }
377
378    fn has_deleted_file(&self, cx: &App) -> bool {
379        self.multibuffer.read(cx).has_deleted_file(cx)
380    }
381
382    fn has_conflict(&self, cx: &App) -> bool {
383        self.multibuffer.read(cx).has_conflict(cx)
384    }
385
386    fn can_save(&self, _: &App) -> bool {
387        true
388    }
389
390    fn save(
391        &mut self,
392        options: SaveOptions,
393        project: Entity<Project>,
394        window: &mut Window,
395        cx: &mut Context<Self>,
396    ) -> Task<Result<()>> {
397        self.editor.save(options, project, window, cx)
398    }
399
400    fn save_as(
401        &mut self,
402        _: Entity<Project>,
403        _: ProjectPath,
404        _window: &mut Window,
405        _: &mut Context<Self>,
406    ) -> Task<Result<()>> {
407        unreachable!()
408    }
409
410    fn reload(
411        &mut self,
412        project: Entity<Project>,
413        window: &mut Window,
414        cx: &mut Context<Self>,
415    ) -> Task<Result<()>> {
416        self.editor.reload(project, window, cx)
417    }
418
419    fn act_as_type<'a>(
420        &'a self,
421        type_id: TypeId,
422        self_handle: &'a Entity<Self>,
423        _: &'a App,
424    ) -> Option<gpui::AnyEntity> {
425        if type_id == TypeId::of::<Self>() {
426            Some(self_handle.clone().into())
427        } else if type_id == TypeId::of::<Editor>() {
428            Some(self.editor.clone().into())
429        } else {
430            None
431        }
432    }
433
434    fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
435        Some(Box::new(self.editor.clone()))
436    }
437
438    fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
439        ToolbarItemLocation::PrimaryLeft
440    }
441
442    fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
443        self.editor.breadcrumbs(theme, cx)
444    }
445
446    fn added_to_workspace(
447        &mut self,
448        workspace: &mut Workspace,
449        window: &mut Window,
450        cx: &mut Context<Self>,
451    ) {
452        self.editor.update(cx, |editor, cx| {
453            editor.added_to_workspace(workspace, window, cx)
454        });
455    }
456}