use std::any::{Any, TypeId}; use collections::HashMap; use dap::StackFrameId; use editor::{ Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, RowHighlightOptions, ToPoint, scroll::Autoscroll, }; use gpui::{ AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, }; use language::{BufferSnapshot, Capability, Point, Selection, SelectionGoal, TreeSitterOptions}; use project::{Project, ProjectPath}; use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div}; use util::ResultExt as _; use workspace::{ Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, item::{BreadcrumbText, ItemEvent}, searchable::SearchableItemHandle, }; use crate::session::running::stack_frame_list::{StackFrameList, StackFrameListEvent}; use anyhow::Result; pub(crate) struct StackTraceView { editor: Entity, multibuffer: Entity, workspace: WeakEntity, project: Entity, stack_frame_list: Entity, selected_stack_frame_id: Option, highlights: Vec<(StackFrameId, Anchor)>, excerpt_for_frames: collections::HashMap, refresh_task: Option>>, _subscription: Option, } impl StackTraceView { pub(crate) fn new( workspace: WeakEntity, project: Entity, stack_frame_list: Entity, window: &mut Window, cx: &mut Context, ) -> Self { let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); let editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); editor.set_vertical_scroll_margin(5, cx); editor }); cx.subscribe_in(&editor, window, |this, editor, event, window, cx| { if let EditorEvent::SelectionsChanged { local: true } = event { let excerpt_id = editor.update(cx, |editor, cx| { let position: Point = editor.selections.newest(cx).head(); editor .snapshot(window, cx) .buffer_snapshot .excerpt_containing(position..position) .map(|excerpt| excerpt.id()) }); if let Some(stack_frame_id) = excerpt_id .and_then(|id| this.excerpt_for_frames.get(&id)) .filter(|id| Some(**id) != this.selected_stack_frame_id) { this.stack_frame_list.update(cx, |list, cx| { list.select_stack_frame_id(*stack_frame_id, window, cx); }); } } }) .detach(); cx.subscribe_in( &stack_frame_list, window, |this, stack_frame_list, event, window, cx| match event { StackFrameListEvent::BuiltEntries => { this.selected_stack_frame_id = stack_frame_list.read(cx).selected_stack_frame_id(); this.update_excerpts(window, cx); } StackFrameListEvent::SelectedStackFrameChanged(selected_frame_id) => { this.selected_stack_frame_id = Some(*selected_frame_id); this.update_highlights(window, cx); if let Some(frame_anchor) = this .highlights .iter() .find(|(frame_id, _)| frame_id == selected_frame_id) .map(|highlight| highlight.1) { this.editor.update(cx, |editor, cx| { if frame_anchor.excerpt_id != editor.selections.newest_anchor().head().excerpt_id { let auto_scroll = Some(Autoscroll::center().for_anchor(frame_anchor)); editor.change_selections(auto_scroll, window, cx, |selections| { let selection_id = selections.new_selection_id(); let selection = Selection { id: selection_id, start: frame_anchor, end: frame_anchor, goal: SelectionGoal::None, reversed: false, }; selections.select_anchors(vec![selection]); }) } }); } } }, ) .detach(); let mut this = Self { editor, multibuffer, workspace, project, excerpt_for_frames: HashMap::default(), highlights: Vec::default(), stack_frame_list, selected_stack_frame_id: None, refresh_task: None, _subscription: None, }; this.update_excerpts(window, cx); this } fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context) { self.refresh_task.take(); self.editor.update(cx, |editor, cx| { editor.clear_highlights::(cx) }); let stack_frames = self .stack_frame_list .update(cx, |list, _| list.flatten_entries(false)); let frames_to_open: Vec<_> = stack_frames .into_iter() .filter_map(|frame| { Some(( frame.id, frame.line as u32 - 1, StackFrameList::abs_path_from_stack_frame(&frame)?, )) }) .collect(); self.multibuffer .update(cx, |multi_buffer, cx| multi_buffer.clear(cx)); let task = cx.spawn_in(window, async move |this, cx| { let mut to_highlights = Vec::default(); for (stack_frame_id, line, abs_path) in frames_to_open { let (worktree, relative_path) = this .update(cx, |this, cx| { this.workspace.update(cx, |workspace, cx| { workspace.project().update(cx, |this, cx| { this.find_or_create_worktree(&abs_path, false, cx) }) }) })?? .await?; let project_path = ProjectPath { worktree_id: worktree.read_with(cx, |tree, _| tree.id())?, path: relative_path.into(), }; if let Some(buffer) = this .read_with(cx, |this, _| this.project.clone())? .update(cx, |project, cx| project.open_buffer(project_path, cx))? .await .log_err() { this.update(cx, |this, cx| { this.multibuffer.update(cx, |multi_buffer, cx| { let line_point = Point::new(line, 0); let start_context = Self::heuristic_syntactic_expand( &buffer.read(cx).snapshot(), line_point, ); // Users will want to see what happened before an active debug line in most cases let range = ExcerptRange { context: start_context..Point::new(line.saturating_add(1), 0), primary: line_point..line_point, }; multi_buffer.push_excerpts(buffer.clone(), vec![range], cx); let line_anchor = multi_buffer.buffer_point_to_anchor(&buffer, line_point, cx); if let Some(line_anchor) = line_anchor { this.excerpt_for_frames .insert(line_anchor.excerpt_id, stack_frame_id); to_highlights.push((stack_frame_id, line_anchor)); } }); }) .ok(); } } this.update_in(cx, |this, window, cx| { this.highlights = to_highlights; this.update_highlights(window, cx); }) .ok(); anyhow::Ok(()) }); self.refresh_task = Some(task); } fn update_highlights(&mut self, window: &mut Window, cx: &mut Context) { self.editor.update(cx, |editor, _| { editor.clear_row_highlights::() }); let stack_frames = self .stack_frame_list .update(cx, |session, _| session.flatten_entries(false)); let active_idx = self .selected_stack_frame_id .and_then(|id| { stack_frames .iter() .enumerate() .find_map(|(idx, frame)| if frame.id == id { Some(idx) } else { None }) }) .unwrap_or(0); self.editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(window, cx).display_snapshot; let first_color = cx.theme().colors().editor_debugger_active_line_background; let color = first_color.opacity(0.5); let mut is_first = true; for (_, highlight) in self.highlights.iter().skip(active_idx) { let position = highlight.to_point(&snapshot.buffer_snapshot); let color = if is_first { is_first = false; first_color } else { color }; let start = snapshot .buffer_snapshot .clip_point(Point::new(position.row, 0), Bias::Left); let end = start + Point::new(1, 0); let start = snapshot.buffer_snapshot.anchor_before(start); let end = snapshot.buffer_snapshot.anchor_before(end); editor.highlight_rows::( start..end, color, RowHighlightOptions::default(), cx, ); } }) } fn heuristic_syntactic_expand(snapshot: &BufferSnapshot, selected_point: Point) -> Point { let mut text_objects = snapshot.text_object_ranges( selected_point..selected_point, TreeSitterOptions::max_start_depth(4), ); let mut start_position = text_objects .find(|(_, obj)| matches!(obj, language::TextObject::AroundFunction)) .map(|(range, _)| snapshot.offset_to_point(range.start)) .map(|point| Point::new(point.row.max(selected_point.row.saturating_sub(8)), 0)) .unwrap_or(selected_point); if start_position.row == selected_point.row { start_position.row = start_position.row.saturating_sub(1); } start_position } } impl Render for StackTraceView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div().size_full().child(self.editor.clone()) } } impl EventEmitter for StackTraceView {} impl Focusable for StackTraceView { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.editor.focus_handle(cx) } } impl Item for StackTraceView { type Event = EditorEvent; fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { Editor::to_item_events(event, f) } fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { self.editor .update(cx, |editor, cx| editor.deactivated(window, cx)); } fn navigate( &mut self, data: Box, window: &mut Window, cx: &mut Context, ) -> bool { self.editor .update(cx, |editor, cx| editor.navigate(data, window, cx)) } fn tab_tooltip_text(&self, _: &App) -> Option { Some("Stack Frame Viewer".into()) } fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString { "Stack Frames".into() } fn for_each_project_item( &self, cx: &App, f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), ) { self.editor.for_each_project_item(cx, f) } fn is_singleton(&self, _: &App) -> bool { false } fn set_nav_history( &mut self, nav_history: ItemNavHistory, _: &mut Window, cx: &mut Context, ) { self.editor.update(cx, |editor, _| { editor.set_nav_history(Some(nav_history)); }); } fn is_dirty(&self, cx: &App) -> bool { self.multibuffer.read(cx).is_dirty(cx) } fn has_deleted_file(&self, cx: &App) -> bool { self.multibuffer.read(cx).has_deleted_file(cx) } fn has_conflict(&self, cx: &App) -> bool { self.multibuffer.read(cx).has_conflict(cx) } fn can_save(&self, _: &App) -> bool { true } fn save( &mut self, format: bool, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { self.editor.save(format, project, window, cx) } fn save_as( &mut self, _: Entity, _: ProjectPath, _window: &mut Window, _: &mut Context, ) -> Task> { unreachable!() } fn reload( &mut self, project: Entity, window: &mut Window, cx: &mut Context, ) -> Task> { self.editor.reload(project, window, cx) } fn act_as_type<'a>( &'a self, type_id: TypeId, self_handle: &'a Entity, _: &'a App, ) -> Option { if type_id == TypeId::of::() { Some(self_handle.to_any()) } else if type_id == TypeId::of::() { Some(self.editor.to_any()) } else { None } } fn as_searchable(&self, _: &Entity) -> Option> { Some(Box::new(self.editor.clone())) } fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft } fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { self.editor.breadcrumbs(theme, cx) } fn added_to_workspace( &mut self, workspace: &mut Workspace, window: &mut Window, cx: &mut Context, ) { self.editor.update(cx, |editor, cx| { editor.added_to_workspace(workspace, window, cx) }); } }