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