use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ actions, elements::{ AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, ParentElement, ScrollTarget, Stack, UniformList, UniformListState, }, fonts::TextStyle, platform::{CursorStyle, MouseButton}, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; use std::{mem, ops::Range, sync::Arc}; use theme::{Theme, ThemeSettings}; use tree_sitter::{Node, TreeCursor}; use workspace::{ item::{Item, ItemHandle}, ToolbarItemLocation, ToolbarItemView, Workspace, }; actions!(debug, [OpenSyntaxTreeView]); pub fn init(cx: &mut AppContext) { cx.add_action( move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| { let active_item = workspace.active_item(cx); let workspace_handle = workspace.weak_handle(); let syntax_tree_view = cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx)); workspace.add_item(Box::new(syntax_tree_view), cx); }, ); } pub struct SyntaxTreeView { workspace_handle: WeakViewHandle, editor: Option, mouse_y: Option, line_height: Option, list_state: UniformListState, selected_descendant_ix: Option, hovered_descendant_ix: Option, } pub struct SyntaxTreeToolbarItemView { tree_view: Option>, subscription: Option, menu_open: bool, } struct EditorState { editor: ViewHandle, active_buffer: Option, _subscription: gpui::Subscription, } #[derive(Clone)] struct BufferState { buffer: ModelHandle, excerpt_id: ExcerptId, active_layer: Option, } impl SyntaxTreeView { pub fn new( workspace_handle: WeakViewHandle, active_item: Option>, cx: &mut ViewContext, ) -> Self { let mut this = Self { workspace_handle: workspace_handle.clone(), list_state: UniformListState::default(), editor: None, mouse_y: None, line_height: None, hovered_descendant_ix: None, selected_descendant_ix: None, }; this.workspace_updated(active_item, cx); cx.observe( &workspace_handle.upgrade(cx).unwrap(), |this, workspace, cx| { this.workspace_updated(workspace.read(cx).active_item(cx), cx); }, ) .detach(); this } fn workspace_updated( &mut self, active_item: Option>, cx: &mut ViewContext, ) { if let Some(item) = active_item { if item.id() != cx.view_id() { if let Some(editor) = item.act_as::(cx) { self.set_editor(editor, cx); } } } } fn set_editor(&mut self, editor: ViewHandle, cx: &mut ViewContext) { if let Some(state) = &self.editor { if state.editor == editor { return; } editor.update(cx, |editor, cx| { editor.clear_background_highlights::(cx) }); } let subscription = cx.subscribe(&editor, |this, _, event, cx| { let did_reparse = match event { editor::Event::Reparsed => true, editor::Event::SelectionsChanged { .. } => false, _ => return, }; this.editor_updated(did_reparse, cx); }); self.editor = Some(EditorState { editor, _subscription: subscription, active_buffer: None, }); self.editor_updated(true, cx); } fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext) -> 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::(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, }); let mut prev_layer = None; if did_reparse { prev_layer = buffer_state.active_layer.take(); } if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id { buffer_state.buffer = buffer.clone(); buffer_state.excerpt_id = excerpt_id; buffer_state.active_layer = None; } 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(); } } // 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_state.scroll_to(ScrollTarget::Show(descendant_ix)); cx.notify(); Some(()) } fn handle_click(&mut self, y: f32, cx: &mut ViewContext) -> Option<()> { let line_height = self.line_height?; let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; self.update_editor_with_range_for_descendant_ix(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]); }); }); Some(()) } fn hover_state_changed(&mut self, cx: &mut ViewContext) { if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) { let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; if self.hovered_descendant_ix != Some(ix) { self.hovered_descendant_ix = Some(ix); self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| { editor.clear_background_highlights::(cx); editor.highlight_background::( vec![range], |theme| theme.editor.document_highlight_write_background, cx, ); }); cx.notify(); } } } fn update_editor_with_range_for_descendant_ix( &self, descendant_ix: usize, cx: &mut ViewContext, mut f: impl FnMut(&mut Editor, Range, &mut ViewContext), ) -> 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(); 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) ..multibuffer.anchor_in_excerpt(excerpt_id, range.end); // Update the editor with the anchor range. editor_state.editor.update(cx, |editor, cx| { f(editor, range, cx); }); Some(()) } fn render_node( cursor: &TreeCursor, depth: u32, selected: bool, hovered: bool, list_hovered: bool, style: &TextStyle, editor_theme: &theme::Editor, cx: &AppContext, ) -> gpui::AnyElement { let node = cursor.node(); let mut range_style = style.clone(); let em_width = style.em_width(cx.font_cache()); let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round(); range_style.color = editor_theme.line_number; let mut anonymous_node_style = style.clone(); let string_color = editor_theme .syntax .highlights .iter() .find_map(|(name, style)| (name == "string").then(|| style.color)?); let property_color = editor_theme .syntax .highlights .iter() .find_map(|(name, style)| (name == "property").then(|| style.color)?); if let Some(color) = string_color { anonymous_node_style.color = color; } let mut row = Flex::row(); if let Some(field_name) = cursor.field_name() { let mut field_style = style.clone(); if let Some(color) = property_color { field_style.color = color; } row.add_children([ Label::new(field_name, field_style), Label::new(": ", style.clone()), ]); } return row .with_child( if node.is_named() { Label::new(node.kind(), style.clone()) } else { Label::new(format!("\"{}\"", node.kind()), anonymous_node_style) } .contained() .with_margin_right(em_width), ) .with_child(Label::new(format_node_range(node), range_style)) .contained() .with_background_color(if selected { editor_theme.selection.selection } else if hovered && list_hovered { editor_theme.active_line_background } else { Default::default() }) .with_padding_left(gutter_padding + depth as f32 * 18.0) .into_any(); } } impl Entity for SyntaxTreeView { type Event = (); } impl View for SyntaxTreeView { fn ui_name() -> &'static str { "SyntaxTreeView" } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { let settings = settings::get::(cx); let font_family_id = settings.buffer_font_family; let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); let font_properties = Default::default(); let font_id = cx .font_cache() .select_font(font_family_id, &font_properties) .unwrap(); let font_size = settings.buffer_font_size(cx); let editor_theme = settings.theme.editor.clone(); let style = TextStyle { color: editor_theme.text_color, font_family_name, font_family_id, font_id, font_size, font_properties: Default::default(), underline: Default::default(), soft_wrap: false, }; let line_height = cx.font_cache().line_height(font_size); if Some(line_height) != self.line_height { self.line_height = Some(line_height); self.hover_state_changed(cx); } if let Some(layer) = self .editor .as_ref() .and_then(|editor| editor.active_buffer.as_ref()) .and_then(|buffer| buffer.active_layer.as_ref()) { let layer = layer.clone(); let theme = editor_theme.clone(); return MouseEventHandler::new::(0, cx, move |state, cx| { let list_hovered = state.hovered(); UniformList::new( self.list_state.clone(), layer.node().descendant_count(), cx, move |this, range, items, cx| { let mut cursor = layer.node().walk(); let mut descendant_ix = range.start as usize; 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 { items.push(Self::render_node( &cursor, depth, Some(descendant_ix) == this.selected_descendant_ix, Some(descendant_ix) == this.hovered_descendant_ix, list_hovered, &style, &theme, cx, )); descendant_ix += 1; if cursor.goto_first_child() { depth += 1; } else { visited_children = true; } } } }, ) }) .on_move(move |event, this, cx| { let y = event.position.y() - event.region.origin_y(); this.mouse_y = Some(y); this.hover_state_changed(cx); }) .on_click(MouseButton::Left, move |event, this, cx| { let y = event.position.y() - event.region.origin_y(); this.handle_click(y, cx); }) .contained() .with_background_color(editor_theme.background) .into_any(); } Empty::new().into_any() } } impl Item for SyntaxTreeView { fn tab_content( &self, _: Option, style: &theme::Tab, _: &AppContext, ) -> gpui::AnyElement { Label::new("Syntax Tree", style.label.clone()).into_any() } fn clone_on_split( &self, _workspace_id: workspace::WorkspaceId, cx: &mut ViewContext, ) -> Option where Self: Sized, { let mut clone = Self::new(self.workspace_handle.clone(), None, cx); if let Some(editor) = &self.editor { clone.set_editor(editor.editor.clone(), cx) } Some(clone) } } impl SyntaxTreeToolbarItemView { pub fn new() -> Self { Self { menu_open: false, tree_view: None, subscription: None, } } fn render_menu( &mut self, cx: &mut ViewContext<'_, '_, Self>, ) -> Option> { let theme = theme::current(cx).clone(); 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(); enum Menu {} Some( Stack::new() .with_child(Self::render_header(&theme, &active_layer, cx)) .with_children(self.menu_open.then(|| { Overlay::new( MouseEventHandler::new::(0, cx, move |_, cx| { Flex::column() .with_children(active_buffer.syntax_layers().enumerate().map( |(ix, layer)| { Self::render_menu_item(&theme, &active_layer, layer, ix, cx) }, )) .contained() .with_style(theme.toolbar_dropdown_menu.container) .constrained() .with_width(400.) .with_height(400.) }) .on_down_out(MouseButton::Left, |_, this, cx| { this.menu_open = false; cx.notify() }), ) .with_hoverable(true) .with_fit_mode(OverlayFitMode::SwitchAnchor) .with_anchor_corner(AnchorCorner::TopLeft) .with_z_index(999) .aligned() .bottom() .left() })) .aligned() .left() .clipped() .into_any(), ) } fn toggle_menu(&mut self, cx: &mut ViewContext) { self.menu_open = !self.menu_open; cx.notify(); } fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext) -> 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; self.menu_open = false; cx.notify(); Some(()) }) } fn render_header( theme: &Arc, active_layer: &OwnedSyntaxLayerInfo, cx: &mut ViewContext, ) -> impl Element { enum ToggleMenu {} MouseEventHandler::new::(0, cx, move |state, _| { let style = theme.toolbar_dropdown_menu.header.style_for(state); Flex::row() .with_child( Label::new(active_layer.language.name().to_string(), style.text.clone()) .contained() .with_margin_right(style.secondary_text_spacing), ) .with_child(Label::new( format_node_range(active_layer.node()), style .secondary_text .clone() .unwrap_or_else(|| style.text.clone()), )) .contained() .with_style(style.container) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { view.toggle_menu(cx); }) } fn render_menu_item( theme: &Arc, active_layer: &OwnedSyntaxLayerInfo, layer: SyntaxLayerInfo, layer_ix: usize, cx: &mut ViewContext, ) -> impl Element { enum ActivateLayer {} MouseEventHandler::new::(layer_ix, cx, move |state, _| { let is_selected = layer.node() == active_layer.node(); let style = theme .toolbar_dropdown_menu .item .in_state(is_selected) .style_for(state); Flex::row() .with_child( Label::new(layer.language.name().to_string(), style.text.clone()) .contained() .with_margin_right(style.secondary_text_spacing), ) .with_child(Label::new( format_node_range(layer.node()), style .secondary_text .clone() .unwrap_or_else(|| style.text.clone()), )) .contained() .with_style(style.container) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { view.select_layer(layer_ix, cx); }) } } 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 Entity for SyntaxTreeToolbarItemView { type Event = (); } impl View for SyntaxTreeToolbarItemView { fn ui_name() -> &'static str { "SyntaxTreeToolbarItemView" } fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement { self.render_menu(cx) .unwrap_or_else(|| Empty::new().into_any()) } } impl ToolbarItemView for SyntaxTreeToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) -> workspace::ToolbarItemLocation { self.menu_open = false; if let Some(item) = active_pane_item { if let Some(view) = item.downcast::() { self.tree_view = Some(view.clone()); self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify())); return ToolbarItemLocation::PrimaryLeft { flex: Some((1., false)), }; } } self.tree_view = None; self.subscription = None; ToolbarItemLocation::Hidden } }