Happy New Years! This PR is a second take at https://github.com/zed-industries/zed/pull/38127 (cc @ConradIrwin) This PR is significantly less complicated than the last attempt: while we still keep our data on the `NavigationHistory` object, we no longer tightly integrate it with the existing back/forward "browser history." Instead, we keep our own stack of `(origin, target)` pairs (in a struct to make it easy to extend with e.g., tag names in the future). The PR is split into two separable commits. Most of the implementation is in the second commit, which: - Defines the stack data structure - Implements `pane::GoToOlderTag` and `pane::GoToNewerTag` in terms of the stack - Hooks into `navigate_to_hover_links` to push tag stack entries This last bit is the most fiddly. The core challenge is that we need to keep track of the `origin` location and calculate the `target` location across three codepaths that might involve creating a new editor and/or splitting the pane. One thing in particular I found difficult was that an editor's `nav_history` (an `ItemNavHistory`) seems to be populated asynchronously. Instead of relying on it, I decided in this code to make my own `ItemNavHistory`. I briefly tried to refactor the code in question, but it seemed like it would significantly increase the scope of the change. I prefer this all-in-one tracking centered around `navigate_to_hover_links ` to the `start/finish` approach taken in https://github.com/zed-industries/zed/commit/b69a2ea200561de9de0edf4d0c4428ee887fb918 because I find it easier to convince myself that the right data is being populated at the right times. Of course, let me know if you think there's a better solution. Closes #14206 Release Notes: - ??? I don't know what to write here! Suggestions welcome
457 lines
15 KiB
Rust
457 lines
15 KiB
Rust
use std::{
|
|
any::{Any, TypeId},
|
|
sync::Arc,
|
|
};
|
|
|
|
use collections::HashMap;
|
|
use dap::StackFrameId;
|
|
use editor::{
|
|
Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
|
|
RowHighlightOptions, SelectionEffects, ToPoint, scroll::Autoscroll,
|
|
};
|
|
use gpui::{
|
|
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, SaveOptions},
|
|
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(&editor.display_snapshot(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.go_to_stack_frame(*stack_frame_id, window, cx).detach();
|
|
});
|
|
}
|
|
}
|
|
})
|
|
.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).opened_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 effects = SelectionEffects::scroll(
|
|
Autoscroll::center().for_anchor(frame_anchor),
|
|
);
|
|
|
|
editor.change_selections(effects, 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
|
|
.read_with(cx, |list, _| list.flatten_entries(false, 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,
|
|
};
|
|
|
|
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
|
|
.read_with(cx, |session, _| session.flatten_entries(false, 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: Arc<dyn Any + Send>,
|
|
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 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,
|
|
options: SaveOptions,
|
|
project: Entity<Project>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<Result<()>> {
|
|
self.editor.save(options, 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<gpui::AnyEntity> {
|
|
if type_id == TypeId::of::<Self>() {
|
|
Some(self_handle.clone().into())
|
|
} else if type_id == TypeId::of::<Editor>() {
|
|
Some(self.editor.clone().into())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> 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)
|
|
});
|
|
}
|
|
}
|