zed/crates/language_tools/src/syntax_tree_view.rs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

513 lines
19 KiB
Rust
Raw Normal View History

use editor::{scroll::Autoscroll, Anchor, Editor, ExcerptId};
2023-06-09 21:55:46 +00:00
use gpui::{
actions, div, rems, uniform_list, AppContext, Div, EventEmitter, FocusHandle, FocusableView,
Hsla, InteractiveElement, IntoElement, Model, MouseButton, MouseDownEvent, MouseMoveEvent,
ParentElement, Render, SharedString, Styled, UniformListScrollHandle, View, ViewContext,
VisualContext, WeakView, WindowContext,
2023-06-09 21:55:46 +00:00
};
2024-01-09 18:53:57 +00:00
use language::{Buffer, OwnedSyntaxLayer};
use std::{mem, ops::Range};
2024-01-10 11:44:08 +00:00
use theme::ActiveTheme;
2023-06-12 22:32:16 +00:00
use tree_sitter::{Node, TreeCursor};
use ui::{h_flex, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu};
2023-06-09 21:55:46 +00:00
use workspace::{
item::{Item, ItemHandle},
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
2023-06-09 21:55:46 +00:00
};
actions!(debug, [OpenSyntaxTreeView]);
2023-06-09 21:55:46 +00:00
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &OpenSyntaxTreeView, cx| {
let active_item = workspace.active_item(cx);
let workspace_handle = workspace.weak_handle();
let syntax_tree_view =
cx.new_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
workspace.split_item(SplitDirection::Right, Box::new(syntax_tree_view), cx)
});
})
.detach();
2023-06-09 21:55:46 +00:00
}
pub struct SyntaxTreeView {
workspace_handle: WeakView<Workspace>,
editor: Option<EditorState>,
list_scroll_handle: UniformListScrollHandle,
selected_descendant_ix: Option<usize>,
hovered_descendant_ix: Option<usize>,
focus_handle: FocusHandle,
}
pub struct SyntaxTreeToolbarItemView {
tree_view: Option<View<SyntaxTreeView>>,
subscription: Option<gpui::Subscription>,
}
struct EditorState {
editor: View<Editor>,
active_buffer: Option<BufferState>,
_subscription: gpui::Subscription,
}
#[derive(Clone)]
struct BufferState {
buffer: Model<Buffer>,
excerpt_id: ExcerptId,
2024-01-09 18:53:57 +00:00
active_layer: Option<OwnedSyntaxLayer>,
2023-06-09 21:55:46 +00:00
}
impl SyntaxTreeView {
pub fn new(
workspace_handle: WeakView<Workspace>,
active_item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> Self {
2023-06-09 21:55:46 +00:00
let mut this = Self {
workspace_handle: workspace_handle.clone(),
list_scroll_handle: UniformListScrollHandle::new(),
2023-06-09 21:55:46 +00:00
editor: None,
hovered_descendant_ix: None,
selected_descendant_ix: None,
focus_handle: cx.focus_handle(),
2023-06-09 21:55:46 +00:00
};
this.workspace_updated(active_item, cx);
2023-06-09 21:55:46 +00:00
cx.observe(
&workspace_handle.upgrade().unwrap(),
2023-06-09 21:55:46 +00:00
|this, workspace, cx| {
this.workspace_updated(workspace.read(cx).active_item(cx), cx);
},
)
.detach();
this
}
fn workspace_updated(
&mut self,
active_item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) {
if let Some(item) = active_item {
if item.item_id() != cx.entity_id() {
2023-06-09 21:55:46 +00:00
if let Some(editor) = item.act_as::<Editor>(cx) {
self.set_editor(editor, cx);
}
}
}
}
fn set_editor(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
if let Some(state) = &self.editor {
if state.editor == editor {
2023-06-09 21:55:46 +00:00
return;
}
editor.update(cx, |editor, cx| {
editor.clear_background_highlights::<Self>(cx)
2023-06-09 21:55:46 +00:00
});
}
let subscription = cx.subscribe(&editor, |this, _, event, cx| {
let did_reparse = match event {
editor::EditorEvent::Reparsed(_) => true,
editor::EditorEvent::SelectionsChanged { .. } => false,
2023-06-09 21:55:46 +00:00
_ => return,
};
this.editor_updated(did_reparse, cx);
2023-06-09 21:55:46 +00:00
});
self.editor = Some(EditorState {
editor,
_subscription: subscription,
active_buffer: None,
});
self.editor_updated(true, cx);
2023-06-09 21:55:46 +00:00
}
fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
// Find which excerpt the cursor is in, and the position within that excerpted buffer.
let editor_state = self.editor.as_mut()?;
let editor = &editor_state.editor.read(cx);
let selection_range = editor.selections.last::<usize>(cx).range();
let multibuffer = editor.buffer().read(cx);
let (buffer, range, excerpt_id) = multibuffer
.range_to_buffer_ranges(selection_range, cx)
.pop()?;
// If the cursor has moved into a different excerpt, retrieve a new syntax layer
// from that buffer.
let buffer_state = editor_state
.active_buffer
.get_or_insert_with(|| BufferState {
buffer: buffer.clone(),
excerpt_id,
active_layer: None,
2023-06-09 21:55:46 +00:00
});
let mut prev_layer = None;
if did_reparse {
prev_layer = buffer_state.active_layer.take();
}
if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt_id {
buffer_state.buffer = buffer.clone();
buffer_state.excerpt_id = excerpt_id;
buffer_state.active_layer = None;
2023-06-09 21:55:46 +00:00
}
let layer = match &mut buffer_state.active_layer {
Some(layer) => layer,
None => {
let snapshot = buffer.read(cx).snapshot();
let layer = if let Some(prev_layer) = prev_layer {
let prev_range = prev_layer.node().byte_range();
snapshot
.syntax_layers()
.filter(|layer| layer.language == &prev_layer.language)
.min_by_key(|layer| {
let range = layer.node().byte_range();
((range.start as i64) - (prev_range.start as i64)).abs()
+ ((range.end as i64) - (prev_range.end as i64)).abs()
})?
} else {
snapshot.syntax_layers().next()?
};
buffer_state.active_layer.insert(layer.to_owned())
}
};
// Within the active layer, find the syntax node under the cursor,
// and scroll to it.
let mut cursor = layer.node().walk();
while cursor.goto_first_child_for_byte(range.start).is_some() {
if !range.is_empty() && cursor.node().end_byte() == range.start {
cursor.goto_next_sibling();
2023-06-09 21:55:46 +00:00
}
}
// Ascend to the smallest ancestor that contains the range.
loop {
let node_range = cursor.node().byte_range();
if node_range.start <= range.start && node_range.end >= range.end {
break;
}
if !cursor.goto_parent() {
break;
}
}
let descendant_ix = cursor.descendant_index();
self.selected_descendant_ix = Some(descendant_ix);
self.list_scroll_handle.scroll_to_item(descendant_ix);
cx.notify();
Some(())
}
fn update_editor_with_range_for_descendant_ix(
2023-06-09 21:55:46 +00:00
&self,
descendant_ix: usize,
2023-06-09 21:55:46 +00:00
cx: &mut ViewContext<Self>,
mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
) -> Option<()> {
let editor_state = self.editor.as_ref()?;
let buffer_state = editor_state.active_buffer.as_ref()?;
let layer = buffer_state.active_layer.as_ref()?;
// Find the node.
let mut cursor = layer.node().walk();
cursor.goto_descendant(descendant_ix);
let node = cursor.node();
2023-06-09 21:55:46 +00:00
let range = node.byte_range();
// Build a text anchor range.
let buffer = buffer_state.buffer.read(cx);
let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
// Build a multibuffer anchor range.
let multibuffer = editor_state.editor.read(cx).buffer();
let multibuffer = multibuffer.read(cx).snapshot(cx);
let excerpt_id = buffer_state.excerpt_id;
let range = multibuffer
.anchor_in_excerpt(excerpt_id, range.start)
.unwrap()
..multibuffer
.anchor_in_excerpt(excerpt_id, range.end)
.unwrap();
// Update the editor with the anchor range.
editor_state.editor.update(cx, |editor, cx| {
f(editor, range, cx);
2023-06-09 21:55:46 +00:00
});
Some(())
2023-06-09 21:55:46 +00:00
}
fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div {
let colors = cx.theme().colors();
let mut row = h_flex();
2023-06-12 22:32:16 +00:00
if let Some(field_name) = cursor.field_name() {
row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
2023-06-12 22:32:16 +00:00
}
let node = cursor.node();
2023-06-12 22:32:16 +00:00
return row
.child(if node.is_named() {
Label::new(node.kind()).color(Color::Default)
} else {
Label::new(format!("\"{}\"", node.kind())).color(Color::Created)
})
.child(
div()
.child(Label::new(format_node_range(node)).color(Color::Muted))
.pl_1(),
)
.text_bg(if selected {
colors.element_selected
} else {
Hsla::default()
})
.pl(rems(depth as f32))
.hover(|style| style.bg(colors.element_hover));
}
2023-06-09 21:55:46 +00:00
}
impl Render for SyntaxTreeView {
fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement {
let mut rendered = div().flex_1();
if let Some(layer) = self
.editor
.as_ref()
.and_then(|editor| editor.active_buffer.as_ref())
.and_then(|buffer| buffer.active_layer.as_ref())
{
2023-06-09 21:55:46 +00:00
let layer = layer.clone();
rendered = rendered.child(uniform_list(
cx.view().clone(),
"SyntaxTreeView",
layer.node().descendant_count(),
move |this, range, cx| {
let mut items = Vec::new();
let mut cursor = layer.node().walk();
let mut descendant_ix = range.start;
cursor.goto_descendant(descendant_ix);
let mut depth = cursor.depth();
let mut visited_children = false;
while descendant_ix < range.end {
if visited_children {
if cursor.goto_next_sibling() {
visited_children = false;
} else if cursor.goto_parent() {
depth -= 1;
} else {
break;
}
} else {
2024-01-10 11:44:08 +00:00
items.push(
Self::render_node(
&cursor,
depth,
Some(descendant_ix) == this.selected_descendant_ix,
cx,
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |tree_view, _: &MouseDownEvent, cx| {
tree_view.update_editor_with_range_for_descendant_ix(
descendant_ix,
cx,
|editor, mut range, cx| {
// Put the cursor at the beginning of the node.
mem::swap(&mut range.start, &mut range.end);
editor.change_selections(
Some(Autoscroll::newest()),
cx,
|selections| {
selections.select_ranges(vec![range]);
},
);
},
);
}),
)
.on_mouse_move(cx.listener(
move |tree_view, _: &MouseMoveEvent, cx| {
if tree_view.hovered_descendant_ix != Some(descendant_ix) {
tree_view.hovered_descendant_ix = Some(descendant_ix);
tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, cx, |editor, range, cx| {
editor.clear_background_highlights::<Self>(cx);
editor.highlight_background::<Self>(
&[range],
2024-01-10 11:44:08 +00:00
|theme| theme.editor_document_highlight_write_background,
cx,
);
});
cx.notify();
}
},
)),
);
descendant_ix += 1;
if cursor.goto_first_child() {
depth += 1;
2023-06-09 21:55:46 +00:00
} else {
visited_children = true;
2023-06-09 21:55:46 +00:00
}
}
}
items
},
)
.size_full()
.track_scroll(self.list_scroll_handle.clone())
.text_bg(cx.theme().colors().background).into_any_element());
2023-06-09 21:55:46 +00:00
}
rendered
}
}
impl EventEmitter<()> for SyntaxTreeView {}
impl FocusableView for SyntaxTreeView {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
2023-06-09 21:55:46 +00:00
}
}
impl Item for SyntaxTreeView {
type Event = ();
fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
Some("Syntax Tree".into())
2023-06-09 21:55:46 +00:00
}
fn telemetry_event_text(&self) -> Option<&'static str> {
None
}
fn clone_on_split(
&self,
_: Option<workspace::WorkspaceId>,
cx: &mut ViewContext<Self>,
) -> Option<View<Self>>
where
Self: Sized,
{
Some(cx.new_view(|cx| {
let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
if let Some(editor) = &self.editor {
clone.set_editor(editor.editor.clone(), cx)
}
clone
}))
}
}
impl SyntaxTreeToolbarItemView {
pub fn new() -> Self {
Self {
tree_view: None,
subscription: None,
}
}
fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option<PopoverMenu<ContextMenu>> {
let tree_view = self.tree_view.as_ref()?;
let tree_view = tree_view.read(cx);
let editor_state = tree_view.editor.as_ref()?;
let buffer_state = editor_state.active_buffer.as_ref()?;
let active_layer = buffer_state.active_layer.clone()?;
let active_buffer = buffer_state.buffer.read(cx).snapshot();
let view = cx.view().clone();
Some(
PopoverMenu::new("Syntax Tree")
.trigger(Self::render_header(&active_layer))
.menu(move |cx| {
ContextMenu::build(cx, |mut menu, cx| {
for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
menu = menu.entry(
format!(
"{} {}",
layer.language.name(),
format_node_range(layer.node())
),
None,
cx.handler_for(&view, move |view, cx| {
view.select_layer(layer_ix, cx);
}),
);
}
menu
})
.into()
}),
)
}
fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
let tree_view = self.tree_view.as_ref()?;
tree_view.update(cx, |view, cx| {
let editor_state = view.editor.as_mut()?;
let buffer_state = editor_state.active_buffer.as_mut()?;
let snapshot = buffer_state.buffer.read(cx).snapshot();
let layer = snapshot.syntax_layers().nth(layer_ix)?;
buffer_state.active_layer = Some(layer.to_owned());
view.selected_descendant_ix = None;
cx.notify();
view.focus_handle.focus(cx);
Some(())
})
}
2024-01-09 18:53:57 +00:00
fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike {
ButtonLike::new("syntax tree header")
.child(Label::new(active_layer.language.name()))
.child(Label::new(format_node_range(active_layer.node())))
}
}
fn format_node_range(node: Node) -> String {
let start = node.start_position();
let end = node.end_position();
format!(
"[{}:{} - {}:{}]",
start.row + 1,
start.column + 1,
end.row + 1,
end.column + 1,
)
}
impl Render for SyntaxTreeToolbarItemView {
fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> impl IntoElement {
self.render_menu(cx)
.unwrap_or_else(|| PopoverMenu::new("Empty Syntax Tree"))
}
}
impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
impl ToolbarItemView for SyntaxTreeToolbarItemView {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
if let Some(item) = active_pane_item {
if let Some(view) = item.downcast::<SyntaxTreeView>() {
self.tree_view = Some(view.clone());
self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
return ToolbarItemLocation::PrimaryLeft;
}
}
self.tree_view = None;
self.subscription = None;
ToolbarItemLocation::Hidden
}
2023-06-09 21:55:46 +00:00
}