debugger: Add stack frame multibuffer (#30395)
This PR adds the ability to expand a debugger stack trace into a multi buffer and view each frame as it's own excerpt. Release Notes: - N/A --------- Co-authored-by: Remco Smits <djsmits12@gmail.com>
This commit is contained in:
parent
6f297132b4
commit
68afe4fdda
10 changed files with 604 additions and 37 deletions
453
crates/debugger_ui/src/stack_trace_view.rs
Normal file
453
crates/debugger_ui/src/stack_trace_view.rs
Normal file
|
@ -0,0 +1,453 @@
|
|||
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<Editor>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
selected_stack_frame_id: Option<StackFrameId>,
|
||||
highlights: Vec<(StackFrameId, Anchor)>,
|
||||
excerpt_for_frames: collections::HashMap<ExcerptId, StackFrameId>,
|
||||
refresh_task: Option<Task<Result<()>>>,
|
||||
_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl StackTraceView {
|
||||
pub(crate) fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
stack_frame_list: Entity<StackFrameList>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> 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>) {
|
||||
self.refresh_task.take();
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.clear_highlights::<DebugStackFrameLine>(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>) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.clear_row_highlights::<DebugStackFrameLine>()
|
||||
});
|
||||
|
||||
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::<DebugStackFrameLine>(
|
||||
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<Self>) -> impl IntoElement {
|
||||
div().size_full().child(self.editor.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> 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>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn Any>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
|
||||
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>,
|
||||
) {
|
||||
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<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.save(format, project, window, cx)
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: Entity<Project>,
|
||||
_: ProjectPath,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.reload(project, window, cx)
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
) -> Option<AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.added_to_workspace(workspace, window, cx)
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue