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