syntax_tree_view.rs

  1use command_palette_hooks::CommandPaletteFilter;
  2use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
  3use gpui::{
  4    App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
  5    Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
  6    ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle,
  7    WeakEntity, Window, actions, div, rems, uniform_list,
  8};
  9use language::{Buffer, OwnedSyntaxLayer};
 10use std::{any::TypeId, mem, ops::Range};
 11use theme::ActiveTheme;
 12use tree_sitter::{Node, TreeCursor};
 13use ui::{
 14    ButtonCommon, ButtonLike, Clickable, Color, ContextMenu, FluentBuilder as _, IconButton,
 15    IconName, Label, LabelCommon, LabelSize, PopoverMenu, StyledExt, Tooltip, h_flex, v_flex,
 16};
 17use workspace::{
 18    Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
 19    ToolbarItemView, Workspace,
 20    item::{Item, ItemHandle},
 21};
 22
 23actions!(
 24    dev,
 25    [
 26        /// Opens the syntax tree view for the current file.
 27        OpenSyntaxTreeView,
 28    ]
 29);
 30
 31actions!(
 32    syntax_tree_view,
 33    [
 34        /// Update the syntax tree view to show the last focused file.
 35        UseActiveEditor
 36    ]
 37);
 38
 39pub fn init(cx: &mut App) {
 40    let syntax_tree_actions = [TypeId::of::<UseActiveEditor>()];
 41
 42    CommandPaletteFilter::update_global(cx, |this, _| {
 43        this.hide_action_types(&syntax_tree_actions);
 44    });
 45
 46    cx.observe_new(move |workspace: &mut Workspace, _, _| {
 47        workspace.register_action(move |workspace, _: &OpenSyntaxTreeView, window, cx| {
 48            CommandPaletteFilter::update_global(cx, |this, _| {
 49                this.show_action_types(&syntax_tree_actions);
 50            });
 51
 52            let active_item = workspace.active_item(cx);
 53            let workspace_handle = workspace.weak_handle();
 54            let syntax_tree_view = cx.new(|cx| {
 55                cx.on_release(move |view: &mut SyntaxTreeView, cx| {
 56                    if view
 57                        .workspace_handle
 58                        .read_with(cx, |workspace, cx| {
 59                            workspace.item_of_type::<SyntaxTreeView>(cx).is_none()
 60                        })
 61                        .unwrap_or_default()
 62                    {
 63                        CommandPaletteFilter::update_global(cx, |this, _| {
 64                            this.hide_action_types(&syntax_tree_actions);
 65                        });
 66                    }
 67                })
 68                .detach();
 69
 70                SyntaxTreeView::new(workspace_handle, active_item, window, cx)
 71            });
 72            workspace.split_item(
 73                SplitDirection::Right,
 74                Box::new(syntax_tree_view),
 75                window,
 76                cx,
 77            )
 78        });
 79        workspace.register_action(|workspace, _: &UseActiveEditor, window, cx| {
 80            if let Some(tree_view) = workspace.item_of_type::<SyntaxTreeView>(cx) {
 81                tree_view.update(cx, |view, cx| {
 82                    view.update_active_editor(&Default::default(), window, cx)
 83                })
 84            }
 85        });
 86    })
 87    .detach();
 88}
 89
 90pub struct SyntaxTreeView {
 91    workspace_handle: WeakEntity<Workspace>,
 92    editor: Option<EditorState>,
 93    list_scroll_handle: UniformListScrollHandle,
 94    /// The last active editor in the workspace. Note that this is specifically not the
 95    /// currently shown editor.
 96    last_active_editor: Option<Entity<Editor>>,
 97    selected_descendant_ix: Option<usize>,
 98    hovered_descendant_ix: Option<usize>,
 99    focus_handle: FocusHandle,
100}
101
102pub struct SyntaxTreeToolbarItemView {
103    tree_view: Option<Entity<SyntaxTreeView>>,
104    subscription: Option<gpui::Subscription>,
105}
106
107struct EditorState {
108    editor: Entity<Editor>,
109    active_buffer: Option<BufferState>,
110    _subscription: gpui::Subscription,
111}
112
113impl EditorState {
114    fn has_language(&self) -> bool {
115        self.active_buffer
116            .as_ref()
117            .is_some_and(|buffer| buffer.active_layer.is_some())
118    }
119}
120
121#[derive(Clone)]
122struct BufferState {
123    buffer: Entity<Buffer>,
124    excerpt_id: ExcerptId,
125    active_layer: Option<OwnedSyntaxLayer>,
126}
127
128impl SyntaxTreeView {
129    pub fn new(
130        workspace_handle: WeakEntity<Workspace>,
131        active_item: Option<Box<dyn ItemHandle>>,
132        window: &mut Window,
133        cx: &mut Context<Self>,
134    ) -> Self {
135        let mut this = Self {
136            workspace_handle: workspace_handle.clone(),
137            list_scroll_handle: UniformListScrollHandle::new(),
138            editor: None,
139            last_active_editor: None,
140            hovered_descendant_ix: None,
141            selected_descendant_ix: None,
142            focus_handle: cx.focus_handle(),
143        };
144
145        this.handle_item_updated(active_item, window, cx);
146
147        cx.subscribe_in(
148            &workspace_handle.upgrade().unwrap(),
149            window,
150            move |this, workspace, event, window, cx| match event {
151                WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
152                    this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
153                }
154                WorkspaceEvent::ItemRemoved { item_id } => {
155                    this.handle_item_removed(item_id, window, cx);
156                }
157                _ => {}
158            },
159        )
160        .detach();
161
162        this
163    }
164
165    fn handle_item_updated(
166        &mut self,
167        active_item: Option<Box<dyn ItemHandle>>,
168        window: &mut Window,
169        cx: &mut Context<Self>,
170    ) {
171        let Some(editor) = active_item
172            .filter(|item| item.item_id() != cx.entity_id())
173            .and_then(|item| item.act_as::<Editor>(cx))
174        else {
175            return;
176        };
177
178        if let Some(editor_state) = self.editor.as_ref().filter(|state| state.has_language()) {
179            self.last_active_editor = (editor_state.editor != editor).then_some(editor);
180        } else {
181            self.set_editor(editor, window, cx);
182        }
183    }
184
185    fn handle_item_removed(
186        &mut self,
187        item_id: &EntityId,
188        window: &mut Window,
189        cx: &mut Context<Self>,
190    ) {
191        if self
192            .editor
193            .as_ref()
194            .is_some_and(|state| state.editor.entity_id() == *item_id)
195        {
196            self.editor = None;
197            // Try activating the last active editor if there is one
198            self.update_active_editor(&Default::default(), window, cx);
199            cx.notify();
200        }
201    }
202
203    fn update_active_editor(
204        &mut self,
205        _: &UseActiveEditor,
206        window: &mut Window,
207        cx: &mut Context<Self>,
208    ) {
209        let Some(editor) = self.last_active_editor.take() else {
210            return;
211        };
212        self.set_editor(editor, window, cx);
213    }
214
215    fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
216        if let Some(state) = &self.editor {
217            if state.editor == editor {
218                return;
219            }
220            editor.update(cx, |editor, cx| {
221                editor.clear_background_highlights::<Self>(cx)
222            });
223        }
224
225        let subscription = cx.subscribe_in(&editor, window, |this, _, event, window, cx| {
226            let did_reparse = match event {
227                editor::EditorEvent::Reparsed(_) => true,
228                editor::EditorEvent::SelectionsChanged { .. } => false,
229                _ => return,
230            };
231            this.editor_updated(did_reparse, window, cx);
232        });
233
234        self.editor = Some(EditorState {
235            editor,
236            _subscription: subscription,
237            active_buffer: None,
238        });
239        self.editor_updated(true, window, cx);
240    }
241
242    fn editor_updated(
243        &mut self,
244        did_reparse: bool,
245        window: &mut Window,
246        cx: &mut Context<Self>,
247    ) -> Option<()> {
248        // Find which excerpt the cursor is in, and the position within that excerpted buffer.
249        let editor_state = self.editor.as_mut()?;
250        let snapshot = editor_state
251            .editor
252            .update(cx, |editor, cx| editor.snapshot(window, cx));
253        let (buffer, range, excerpt_id) = editor_state.editor.update(cx, |editor, cx| {
254            let selection_range = editor.selections.last::<usize>(cx).range();
255            let multi_buffer = editor.buffer().read(cx);
256            let (buffer, range, excerpt_id) = snapshot
257                .buffer_snapshot
258                .range_to_buffer_ranges(selection_range)
259                .pop()?;
260            let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap();
261            Some((buffer, range, excerpt_id))
262        })?;
263
264        // If the cursor has moved into a different excerpt, retrieve a new syntax layer
265        // from that buffer.
266        let buffer_state = editor_state
267            .active_buffer
268            .get_or_insert_with(|| BufferState {
269                buffer: buffer.clone(),
270                excerpt_id,
271                active_layer: None,
272            });
273        let mut prev_layer = None;
274        if did_reparse {
275            prev_layer = buffer_state.active_layer.take();
276        }
277        if buffer_state.buffer != buffer || buffer_state.excerpt_id != excerpt_id {
278            buffer_state.buffer = buffer.clone();
279            buffer_state.excerpt_id = excerpt_id;
280            buffer_state.active_layer = None;
281        }
282
283        let layer = match &mut buffer_state.active_layer {
284            Some(layer) => layer,
285            None => {
286                let snapshot = buffer.read(cx).snapshot();
287                let layer = if let Some(prev_layer) = prev_layer {
288                    let prev_range = prev_layer.node().byte_range();
289                    snapshot
290                        .syntax_layers()
291                        .filter(|layer| layer.language == &prev_layer.language)
292                        .min_by_key(|layer| {
293                            let range = layer.node().byte_range();
294                            ((range.start as i64) - (prev_range.start as i64)).abs()
295                                + ((range.end as i64) - (prev_range.end as i64)).abs()
296                        })?
297                } else {
298                    snapshot.syntax_layers().next()?
299                };
300                buffer_state.active_layer.insert(layer.to_owned())
301            }
302        };
303
304        // Within the active layer, find the syntax node under the cursor,
305        // and scroll to it.
306        let mut cursor = layer.node().walk();
307        while cursor.goto_first_child_for_byte(range.start).is_some() {
308            if !range.is_empty() && cursor.node().end_byte() == range.start {
309                cursor.goto_next_sibling();
310            }
311        }
312
313        // Ascend to the smallest ancestor that contains the range.
314        loop {
315            let node_range = cursor.node().byte_range();
316            if node_range.start <= range.start && node_range.end >= range.end {
317                break;
318            }
319            if !cursor.goto_parent() {
320                break;
321            }
322        }
323
324        let descendant_ix = cursor.descendant_index();
325        self.selected_descendant_ix = Some(descendant_ix);
326        self.list_scroll_handle
327            .scroll_to_item(descendant_ix, ScrollStrategy::Center);
328
329        cx.notify();
330        Some(())
331    }
332
333    fn update_editor_with_range_for_descendant_ix(
334        &self,
335        descendant_ix: usize,
336        window: &mut Window,
337        cx: &mut Context<Self>,
338        mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut Window, &mut Context<Editor>),
339    ) -> Option<()> {
340        let editor_state = self.editor.as_ref()?;
341        let buffer_state = editor_state.active_buffer.as_ref()?;
342        let layer = buffer_state.active_layer.as_ref()?;
343
344        // Find the node.
345        let mut cursor = layer.node().walk();
346        cursor.goto_descendant(descendant_ix);
347        let node = cursor.node();
348        let range = node.byte_range();
349
350        // Build a text anchor range.
351        let buffer = buffer_state.buffer.read(cx);
352        let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
353
354        // Build a multibuffer anchor range.
355        let multibuffer = editor_state.editor.read(cx).buffer();
356        let multibuffer = multibuffer.read(cx).snapshot(cx);
357        let excerpt_id = buffer_state.excerpt_id;
358        let range = multibuffer
359            .anchor_in_excerpt(excerpt_id, range.start)
360            .unwrap()
361            ..multibuffer
362                .anchor_in_excerpt(excerpt_id, range.end)
363                .unwrap();
364
365        // Update the editor with the anchor range.
366        editor_state.editor.update(cx, |editor, cx| {
367            f(editor, range, window, cx);
368        });
369        Some(())
370    }
371
372    fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &App) -> Div {
373        let colors = cx.theme().colors();
374        let mut row = h_flex();
375        if let Some(field_name) = cursor.field_name() {
376            row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
377        }
378
379        let node = cursor.node();
380        row.child(if node.is_named() {
381            Label::new(node.kind()).color(Color::Default)
382        } else {
383            Label::new(format!("\"{}\"", node.kind())).color(Color::Created)
384        })
385        .child(
386            div()
387                .child(Label::new(format_node_range(node)).color(Color::Muted))
388                .pl_1(),
389        )
390        .text_bg(if selected {
391            colors.element_selected
392        } else {
393            Hsla::default()
394        })
395        .pl(rems(depth as f32))
396        .hover(|style| style.bg(colors.element_hover))
397    }
398
399    fn compute_items(
400        &mut self,
401        layer: &OwnedSyntaxLayer,
402        range: Range<usize>,
403        cx: &Context<Self>,
404    ) -> Vec<Div> {
405        let mut items = Vec::new();
406        let mut cursor = layer.node().walk();
407        let mut descendant_ix = range.start;
408        cursor.goto_descendant(descendant_ix);
409        let mut depth = cursor.depth();
410        let mut visited_children = false;
411        while descendant_ix < range.end {
412            if visited_children {
413                if cursor.goto_next_sibling() {
414                    visited_children = false;
415                } else if cursor.goto_parent() {
416                    depth -= 1;
417                } else {
418                    break;
419                }
420            } else {
421                items.push(
422                    Self::render_node(
423                        &cursor,
424                        depth,
425                        Some(descendant_ix) == self.selected_descendant_ix,
426                        cx,
427                    )
428                    .on_mouse_down(
429                        MouseButton::Left,
430                        cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
431                            tree_view.update_editor_with_range_for_descendant_ix(
432                                descendant_ix,
433                                window,
434                                cx,
435                                |editor, mut range, window, cx| {
436                                    // Put the cursor at the beginning of the node.
437                                    mem::swap(&mut range.start, &mut range.end);
438
439                                    editor.change_selections(
440                                        SelectionEffects::scroll(Autoscroll::newest()),
441                                        window,
442                                        cx,
443                                        |selections| {
444                                            selections.select_ranges(vec![range]);
445                                        },
446                                    );
447                                },
448                            );
449                        }),
450                    )
451                    .on_mouse_move(cx.listener(
452                        move |tree_view, _: &MouseMoveEvent, window, cx| {
453                            if tree_view.hovered_descendant_ix != Some(descendant_ix) {
454                                tree_view.hovered_descendant_ix = Some(descendant_ix);
455                                tree_view.update_editor_with_range_for_descendant_ix(
456                                    descendant_ix,
457                                    window,
458                                    cx,
459                                    |editor, range, _, cx| {
460                                        editor.clear_background_highlights::<Self>(cx);
461                                        editor.highlight_background::<Self>(
462                                            &[range],
463                                            |theme| {
464                                                theme
465                                                    .colors()
466                                                    .editor_document_highlight_write_background
467                                            },
468                                            cx,
469                                        );
470                                    },
471                                );
472                                cx.notify();
473                            }
474                        },
475                    )),
476                );
477                descendant_ix += 1;
478                if cursor.goto_first_child() {
479                    depth += 1;
480                } else {
481                    visited_children = true;
482                }
483            }
484        }
485        items
486    }
487}
488
489impl Render for SyntaxTreeView {
490    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
491        div()
492            .flex_1()
493            .bg(cx.theme().colors().editor_background)
494            .map(|this| {
495                let editor_state = self.editor.as_ref();
496
497                if let Some(layer) = editor_state
498                    .and_then(|editor| editor.active_buffer.as_ref())
499                    .and_then(|buffer| buffer.active_layer.as_ref())
500                {
501                    let layer = layer.clone();
502                    this.child(
503                        uniform_list(
504                            "SyntaxTreeView",
505                            layer.node().descendant_count(),
506                            cx.processor(move |this, range: Range<usize>, _, cx| {
507                                this.compute_items(&layer, range, cx)
508                            }),
509                        )
510                        .size_full()
511                        .track_scroll(self.list_scroll_handle.clone())
512                        .text_bg(cx.theme().colors().background)
513                        .into_any_element(),
514                    )
515                } else {
516                    let inner_content = v_flex()
517                        .items_center()
518                        .text_center()
519                        .gap_2()
520                        .max_w_3_5()
521                        .map(|this| {
522                            if editor_state.is_some_and(|state| !state.has_language()) {
523                                this.child(Label::new("Current editor has no associated language"))
524                                    .child(
525                                        Label::new(concat!(
526                                            "Try assigning a language or",
527                                            "switching to a different buffer"
528                                        ))
529                                        .size(LabelSize::Small),
530                                    )
531                            } else {
532                                this.child(Label::new("Not attached to an editor")).child(
533                                    Label::new("Focus an editor to show a new tree view")
534                                        .size(LabelSize::Small),
535                                )
536                            }
537                        });
538
539                    this.h_flex()
540                        .size_full()
541                        .justify_center()
542                        .child(inner_content)
543                }
544            })
545    }
546}
547
548impl EventEmitter<()> for SyntaxTreeView {}
549
550impl Focusable for SyntaxTreeView {
551    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
552        self.focus_handle.clone()
553    }
554}
555
556impl Item for SyntaxTreeView {
557    type Event = ();
558
559    fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {}
560
561    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
562        "Syntax Tree".into()
563    }
564
565    fn telemetry_event_text(&self) -> Option<&'static str> {
566        None
567    }
568
569    fn clone_on_split(
570        &self,
571        _: Option<workspace::WorkspaceId>,
572        window: &mut Window,
573        cx: &mut Context<Self>,
574    ) -> Option<Entity<Self>>
575    where
576        Self: Sized,
577    {
578        Some(cx.new(|cx| {
579            let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
580            if let Some(editor) = &self.editor {
581                clone.set_editor(editor.editor.clone(), window, cx)
582            }
583            clone
584        }))
585    }
586}
587
588impl Default for SyntaxTreeToolbarItemView {
589    fn default() -> Self {
590        Self::new()
591    }
592}
593
594impl SyntaxTreeToolbarItemView {
595    pub fn new() -> Self {
596        Self {
597            tree_view: None,
598            subscription: None,
599        }
600    }
601
602    fn render_menu(&mut self, cx: &mut Context<Self>) -> Option<PopoverMenu<ContextMenu>> {
603        let tree_view = self.tree_view.as_ref()?;
604        let tree_view = tree_view.read(cx);
605
606        let editor_state = tree_view.editor.as_ref()?;
607        let buffer_state = editor_state.active_buffer.as_ref()?;
608        let active_layer = buffer_state.active_layer.clone()?;
609        let active_buffer = buffer_state.buffer.read(cx).snapshot();
610
611        let view = cx.entity();
612        Some(
613            PopoverMenu::new("Syntax Tree")
614                .trigger(Self::render_header(&active_layer))
615                .menu(move |window, cx| {
616                    ContextMenu::build(window, cx, |mut menu, window, _| {
617                        for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
618                            menu = menu.entry(
619                                format!(
620                                    "{} {}",
621                                    layer.language.name(),
622                                    format_node_range(layer.node())
623                                ),
624                                None,
625                                window.handler_for(&view, move |view, window, cx| {
626                                    view.select_layer(layer_ix, window, cx);
627                                }),
628                            );
629                        }
630                        menu
631                    })
632                    .into()
633                }),
634        )
635    }
636
637    fn select_layer(
638        &mut self,
639        layer_ix: usize,
640        window: &mut Window,
641        cx: &mut Context<Self>,
642    ) -> Option<()> {
643        let tree_view = self.tree_view.as_ref()?;
644        tree_view.update(cx, |view, cx| {
645            let editor_state = view.editor.as_mut()?;
646            let buffer_state = editor_state.active_buffer.as_mut()?;
647            let snapshot = buffer_state.buffer.read(cx).snapshot();
648            let layer = snapshot.syntax_layers().nth(layer_ix)?;
649            buffer_state.active_layer = Some(layer.to_owned());
650            view.selected_descendant_ix = None;
651            cx.notify();
652            view.focus_handle.focus(window);
653            Some(())
654        })
655    }
656
657    fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike {
658        ButtonLike::new("syntax tree header")
659            .child(Label::new(active_layer.language.name()))
660            .child(Label::new(format_node_range(active_layer.node())))
661    }
662
663    fn render_update_button(&mut self, cx: &mut Context<Self>) -> Option<IconButton> {
664        self.tree_view.as_ref().and_then(|view| {
665            view.update(cx, |view, cx| {
666                view.last_active_editor.as_ref().map(|editor| {
667                    IconButton::new("syntax-view-update", IconName::RotateCw)
668                        .tooltip({
669                            let active_tab_name = editor.read_with(cx, |editor, cx| {
670                                editor.tab_content_text(Default::default(), cx)
671                            });
672
673                            Tooltip::text(format!("Update view to '{active_tab_name}'"))
674                        })
675                        .on_click(cx.listener(|this, _, window, cx| {
676                            this.update_active_editor(&Default::default(), window, cx);
677                        }))
678                })
679            })
680        })
681    }
682}
683
684fn format_node_range(node: Node) -> String {
685    let start = node.start_position();
686    let end = node.end_position();
687    format!(
688        "[{}:{} - {}:{}]",
689        start.row + 1,
690        start.column + 1,
691        end.row + 1,
692        end.column + 1,
693    )
694}
695
696impl Render for SyntaxTreeToolbarItemView {
697    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
698        h_flex()
699            .gap_1()
700            .children(self.render_menu(cx))
701            .children(self.render_update_button(cx))
702    }
703}
704
705impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
706
707impl ToolbarItemView for SyntaxTreeToolbarItemView {
708    fn set_active_pane_item(
709        &mut self,
710        active_pane_item: Option<&dyn ItemHandle>,
711        window: &mut Window,
712        cx: &mut Context<Self>,
713    ) -> ToolbarItemLocation {
714        if let Some(item) = active_pane_item
715            && let Some(view) = item.downcast::<SyntaxTreeView>()
716        {
717            self.tree_view = Some(view.clone());
718            self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
719            return ToolbarItemLocation::PrimaryLeft;
720        }
721        self.tree_view = None;
722        self.subscription = None;
723        ToolbarItemLocation::Hidden
724    }
725}