syntax_tree_view.rs

  1use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
  2use gpui::{
  3    actions,
  4    elements::{
  5        AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
  6        ParentElement, ScrollTarget, Stack, UniformList, UniformListState,
  7    },
  8    fonts::TextStyle,
  9    platform::{CursorStyle, MouseButton},
 10    AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle,
 11};
 12use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo};
 13use std::{ops::Range, sync::Arc};
 14use theme::{Theme, ThemeSettings};
 15use tree_sitter::Node;
 16use workspace::{
 17    item::{Item, ItemHandle},
 18    ToolbarItemLocation, ToolbarItemView, Workspace,
 19};
 20
 21actions!(log, [OpenSyntaxTreeView]);
 22
 23pub fn init(cx: &mut AppContext) {
 24    cx.add_action(
 25        move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| {
 26            let active_item = workspace.active_item(cx);
 27            let workspace_handle = workspace.weak_handle();
 28            let syntax_tree_view =
 29                cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
 30            workspace.add_item(Box::new(syntax_tree_view), cx);
 31        },
 32    );
 33}
 34
 35pub struct SyntaxTreeView {
 36    workspace_handle: WeakViewHandle<Workspace>,
 37    editor: Option<EditorState>,
 38    mouse_y: Option<f32>,
 39    line_height: Option<f32>,
 40    list_state: UniformListState,
 41    selected_descendant_ix: Option<usize>,
 42    hovered_descendant_ix: Option<usize>,
 43}
 44
 45pub struct SyntaxTreeToolbarItemView {
 46    tree_view: Option<ViewHandle<SyntaxTreeView>>,
 47    subscription: Option<gpui::Subscription>,
 48    menu_open: bool,
 49}
 50
 51struct EditorState {
 52    editor: ViewHandle<Editor>,
 53    active_buffer: Option<BufferState>,
 54    _subscription: gpui::Subscription,
 55}
 56
 57#[derive(Clone)]
 58struct BufferState {
 59    buffer: ModelHandle<Buffer>,
 60    excerpt_id: ExcerptId,
 61    active_layer: Option<OwnedSyntaxLayerInfo>,
 62}
 63
 64impl SyntaxTreeView {
 65    pub fn new(
 66        workspace_handle: WeakViewHandle<Workspace>,
 67        active_item: Option<Box<dyn ItemHandle>>,
 68        cx: &mut ViewContext<Self>,
 69    ) -> Self {
 70        let mut this = Self {
 71            workspace_handle: workspace_handle.clone(),
 72            list_state: UniformListState::default(),
 73            editor: None,
 74            mouse_y: None,
 75            line_height: None,
 76            hovered_descendant_ix: None,
 77            selected_descendant_ix: None,
 78        };
 79
 80        this.workspace_updated(active_item, cx);
 81        cx.observe(
 82            &workspace_handle.upgrade(cx).unwrap(),
 83            |this, workspace, cx| {
 84                this.workspace_updated(workspace.read(cx).active_item(cx), cx);
 85            },
 86        )
 87        .detach();
 88
 89        this
 90    }
 91
 92    fn workspace_updated(
 93        &mut self,
 94        active_item: Option<Box<dyn ItemHandle>>,
 95        cx: &mut ViewContext<Self>,
 96    ) {
 97        if let Some(item) = active_item {
 98            if item.id() != cx.view_id() {
 99                if let Some(editor) = item.act_as::<Editor>(cx) {
100                    self.set_editor(editor, cx);
101                }
102            }
103        }
104    }
105
106    fn set_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
107        if let Some(state) = &self.editor {
108            if state.editor == editor {
109                return;
110            }
111            editor.update(cx, |editor, cx| {
112                editor.clear_background_highlights::<Self>(cx)
113            });
114        }
115
116        let subscription = cx.subscribe(&editor, |this, _, event, cx| {
117            let did_reparse = match event {
118                editor::Event::Reparsed => true,
119                editor::Event::SelectionsChanged { .. } => false,
120                _ => return,
121            };
122            this.editor_updated(did_reparse, cx);
123        });
124
125        self.editor = Some(EditorState {
126            editor,
127            _subscription: subscription,
128            active_buffer: None,
129        });
130        self.editor_updated(true, cx);
131    }
132
133    fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
134        // Find which excerpt the cursor is in, and the position within that excerpted buffer.
135        let editor_state = self.editor.as_mut()?;
136        let editor = &editor_state.editor.read(cx);
137        let selection_range = editor.selections.last::<usize>(cx).range();
138        let multibuffer = editor.buffer().read(cx);
139        let (buffer, range, excerpt_id) = multibuffer
140            .range_to_buffer_ranges(selection_range, cx)
141            .pop()?;
142
143        // If the cursor has moved into a different excerpt, retrieve a new syntax layer
144        // from that buffer.
145        let buffer_state = editor_state
146            .active_buffer
147            .get_or_insert_with(|| BufferState {
148                buffer: buffer.clone(),
149                excerpt_id,
150                active_layer: None,
151            });
152        let mut prev_layer = None;
153        if did_reparse {
154            prev_layer = buffer_state.active_layer.take();
155        }
156        if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id {
157            buffer_state.buffer = buffer.clone();
158            buffer_state.excerpt_id = excerpt_id;
159            buffer_state.active_layer = None;
160        }
161
162        let layer = match &mut buffer_state.active_layer {
163            Some(layer) => layer,
164            None => {
165                let snapshot = buffer.read(cx).snapshot();
166                let layer = if let Some(prev_layer) = prev_layer {
167                    let prev_range = prev_layer.node().byte_range();
168                    snapshot
169                        .syntax_layers()
170                        .filter(|layer| layer.language == &prev_layer.language)
171                        .min_by_key(|layer| {
172                            let range = layer.node().byte_range();
173                            ((range.start as i64) - (prev_range.start as i64)).abs()
174                                + ((range.end as i64) - (prev_range.end as i64)).abs()
175                        })?
176                } else {
177                    snapshot.syntax_layers().next()?
178                };
179                buffer_state.active_layer.insert(layer.to_owned())
180            }
181        };
182
183        // Within the active layer, find the syntax node under the cursor,
184        // and scroll to it.
185        let mut cursor = layer.node().walk();
186        while cursor.goto_first_child_for_byte(range.start).is_some() {
187            if !range.is_empty() && cursor.node().end_byte() == range.start {
188                cursor.goto_next_sibling();
189            }
190        }
191
192        // Ascend to the smallest ancestor that contains the range.
193        loop {
194            let node_range = cursor.node().byte_range();
195            if node_range.start <= range.start && node_range.end >= range.end {
196                break;
197            }
198            if !cursor.goto_parent() {
199                break;
200            }
201        }
202
203        let descendant_ix = cursor.descendant_index();
204        self.selected_descendant_ix = Some(descendant_ix);
205        self.list_state.scroll_to(ScrollTarget::Show(descendant_ix));
206
207        cx.notify();
208        Some(())
209    }
210
211    fn handle_click(&mut self, y: f32, cx: &mut ViewContext<SyntaxTreeView>) -> Option<()> {
212        let line_height = self.line_height?;
213        let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
214
215        self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
216            editor.change_selections(Some(Autoscroll::newest()), cx, |selections| {
217                selections.select_ranges(vec![range]);
218            });
219        });
220        Some(())
221    }
222
223    fn hover_state_changed(&mut self, cx: &mut ViewContext<SyntaxTreeView>) {
224        if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) {
225            let ix = ((self.list_state.scroll_top() + y) / line_height) as usize;
226            if self.hovered_descendant_ix != Some(ix) {
227                self.hovered_descendant_ix = Some(ix);
228                self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| {
229                    editor.clear_background_highlights::<Self>(cx);
230                    editor.highlight_background::<Self>(
231                        vec![range],
232                        |theme| theme.editor.document_highlight_write_background,
233                        cx,
234                    );
235                });
236                cx.notify();
237            }
238        }
239    }
240
241    fn update_editor_with_range_for_descendant_ix(
242        &self,
243        descendant_ix: usize,
244        cx: &mut ViewContext<Self>,
245        mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
246    ) -> Option<()> {
247        let editor_state = self.editor.as_ref()?;
248        let buffer_state = editor_state.active_buffer.as_ref()?;
249        let layer = buffer_state.active_layer.as_ref()?;
250
251        // Find the node.
252        let mut cursor = layer.node().walk();
253        cursor.goto_descendant(descendant_ix);
254        let node = cursor.node();
255        let range = node.byte_range();
256
257        // Build a text anchor range.
258        let buffer = buffer_state.buffer.read(cx);
259        let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end);
260
261        // Build a multibuffer anchor range.
262        let multibuffer = editor_state.editor.read(cx).buffer();
263        let multibuffer = multibuffer.read(cx).snapshot(cx);
264        let excerpt_id = buffer_state.excerpt_id;
265        let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start)
266            ..multibuffer.anchor_in_excerpt(excerpt_id, range.end);
267
268        // Update the editor with the anchor range.
269        editor_state.editor.update(cx, |editor, cx| {
270            f(editor, range, cx);
271        });
272        Some(())
273    }
274
275    fn render_node(
276        node: Node,
277        depth: u32,
278        selected: bool,
279        hovered: bool,
280        list_hovered: bool,
281        style: &TextStyle,
282        editor_theme: &theme::Editor,
283        cx: &AppContext,
284    ) -> gpui::AnyElement<SyntaxTreeView> {
285        let mut range_style = style.clone();
286        let mut anonymous_node_style = style.clone();
287        let em_width = style.em_width(cx.font_cache());
288        let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round();
289
290        range_style.color = editor_theme.line_number;
291
292        let string_color = editor_theme
293            .syntax
294            .highlights
295            .iter()
296            .find_map(|(name, style)| (name == "string").then(|| style.color)?);
297        if let Some(color) = string_color {
298            anonymous_node_style.color = color;
299        }
300
301        Flex::row()
302            .with_child(
303                if node.is_named() {
304                    Label::new(node.kind(), style.clone())
305                } else {
306                    Label::new(format!("\"{}\"", node.kind()), anonymous_node_style)
307                }
308                .contained()
309                .with_margin_right(em_width),
310            )
311            .with_child(Label::new(format_node_range(node), range_style))
312            .contained()
313            .with_background_color(if selected {
314                editor_theme.selection.selection
315            } else if hovered && list_hovered {
316                editor_theme.active_line_background
317            } else {
318                Default::default()
319            })
320            .with_padding_left(gutter_padding + depth as f32 * 18.0)
321            .into_any()
322    }
323}
324
325impl Entity for SyntaxTreeView {
326    type Event = ();
327}
328
329impl View for SyntaxTreeView {
330    fn ui_name() -> &'static str {
331        "SyntaxTreeView"
332    }
333
334    fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
335        let settings = settings::get::<ThemeSettings>(cx);
336        let font_family_id = settings.buffer_font_family;
337        let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
338        let font_properties = Default::default();
339        let font_id = cx
340            .font_cache()
341            .select_font(font_family_id, &font_properties)
342            .unwrap();
343        let font_size = settings.buffer_font_size(cx);
344
345        let editor_theme = settings.theme.editor.clone();
346        let style = TextStyle {
347            color: editor_theme.text_color,
348            font_family_name,
349            font_family_id,
350            font_id,
351            font_size,
352            font_properties: Default::default(),
353            underline: Default::default(),
354        };
355
356        let line_height = cx.font_cache().line_height(font_size);
357        if Some(line_height) != self.line_height {
358            self.line_height = Some(line_height);
359            self.hover_state_changed(cx);
360        }
361
362        if let Some(layer) = self
363            .editor
364            .as_ref()
365            .and_then(|editor| editor.active_buffer.as_ref())
366            .and_then(|buffer| buffer.active_layer.as_ref())
367        {
368            let layer = layer.clone();
369            let theme = editor_theme.clone();
370            return MouseEventHandler::<Self, Self>::new(0, cx, move |state, cx| {
371                let list_hovered = state.hovered();
372                UniformList::new(
373                    self.list_state.clone(),
374                    layer.node().descendant_count(),
375                    cx,
376                    move |this, range, items, cx| {
377                        let mut cursor = layer.node().walk();
378                        let mut descendant_ix = range.start as usize;
379                        cursor.goto_descendant(descendant_ix);
380                        let mut depth = cursor.depth();
381                        let mut visited_children = false;
382                        while descendant_ix < range.end {
383                            if visited_children {
384                                if cursor.goto_next_sibling() {
385                                    visited_children = false;
386                                } else if cursor.goto_parent() {
387                                    depth -= 1;
388                                } else {
389                                    break;
390                                }
391                            } else {
392                                items.push(Self::render_node(
393                                    cursor.node(),
394                                    depth,
395                                    Some(descendant_ix) == this.selected_descendant_ix,
396                                    Some(descendant_ix) == this.hovered_descendant_ix,
397                                    list_hovered,
398                                    &style,
399                                    &theme,
400                                    cx,
401                                ));
402                                descendant_ix += 1;
403                                if cursor.goto_first_child() {
404                                    depth += 1;
405                                } else {
406                                    visited_children = true;
407                                }
408                            }
409                        }
410                    },
411                )
412            })
413            .on_move(move |event, this, cx| {
414                let y = event.position.y() - event.region.origin_y();
415                this.mouse_y = Some(y);
416                this.hover_state_changed(cx);
417            })
418            .on_click(MouseButton::Left, move |event, this, cx| {
419                let y = event.position.y() - event.region.origin_y();
420                this.handle_click(y, cx);
421            })
422            .contained()
423            .with_background_color(editor_theme.background)
424            .into_any();
425        }
426
427        Empty::new().into_any()
428    }
429}
430
431impl Item for SyntaxTreeView {
432    fn tab_content<V: View>(
433        &self,
434        _: Option<usize>,
435        style: &theme::Tab,
436        _: &AppContext,
437    ) -> gpui::AnyElement<V> {
438        Label::new("Syntax Tree", style.label.clone()).into_any()
439    }
440
441    fn clone_on_split(
442        &self,
443        _workspace_id: workspace::WorkspaceId,
444        cx: &mut ViewContext<Self>,
445    ) -> Option<Self>
446    where
447        Self: Sized,
448    {
449        let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
450        if let Some(editor) = &self.editor {
451            clone.set_editor(editor.editor.clone(), cx)
452        }
453        Some(clone)
454    }
455}
456
457impl SyntaxTreeToolbarItemView {
458    pub fn new() -> Self {
459        Self {
460            menu_open: false,
461            tree_view: None,
462            subscription: None,
463        }
464    }
465
466    fn render_menu(
467        &mut self,
468        cx: &mut ViewContext<'_, '_, Self>,
469    ) -> Option<gpui::AnyElement<Self>> {
470        let theme = theme::current(cx).clone();
471        let tree_view = self.tree_view.as_ref()?;
472        let tree_view = tree_view.read(cx);
473
474        let editor_state = tree_view.editor.as_ref()?;
475        let buffer_state = editor_state.active_buffer.as_ref()?;
476        let active_layer = buffer_state.active_layer.clone()?;
477        let active_buffer = buffer_state.buffer.read(cx).snapshot();
478
479        enum Menu {}
480
481        Some(
482            Stack::new()
483                .with_child(Self::render_header(&theme, &active_layer, cx))
484                .with_children(self.menu_open.then(|| {
485                    Overlay::new(
486                        MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
487                            Flex::column()
488                                .with_children(active_buffer.syntax_layers().enumerate().map(
489                                    |(ix, layer)| {
490                                        Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
491                                    },
492                                ))
493                                .contained()
494                                .with_style(theme.toolbar_dropdown_menu.container)
495                                .constrained()
496                                .with_width(400.)
497                                .with_height(400.)
498                        })
499                        .on_down_out(MouseButton::Left, |_, this, cx| {
500                            this.menu_open = false;
501                            cx.notify()
502                        }),
503                    )
504                    .with_hoverable(true)
505                    .with_fit_mode(OverlayFitMode::SwitchAnchor)
506                    .with_anchor_corner(AnchorCorner::TopLeft)
507                    .with_z_index(999)
508                    .aligned()
509                    .bottom()
510                    .left()
511                }))
512                .aligned()
513                .left()
514                .clipped()
515                .into_any(),
516        )
517    }
518
519    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
520        self.menu_open = !self.menu_open;
521        cx.notify();
522    }
523
524    fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
525        let tree_view = self.tree_view.as_ref()?;
526        tree_view.update(cx, |view, cx| {
527            let editor_state = view.editor.as_mut()?;
528            let buffer_state = editor_state.active_buffer.as_mut()?;
529            let snapshot = buffer_state.buffer.read(cx).snapshot();
530            let layer = snapshot.syntax_layers().nth(layer_ix)?;
531            buffer_state.active_layer = Some(layer.to_owned());
532            view.selected_descendant_ix = None;
533            self.menu_open = false;
534            cx.notify();
535            Some(())
536        })
537    }
538
539    fn render_header(
540        theme: &Arc<Theme>,
541        active_layer: &OwnedSyntaxLayerInfo,
542        cx: &mut ViewContext<Self>,
543    ) -> impl Element<Self> {
544        enum ToggleMenu {}
545        MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
546            let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
547            Flex::row()
548                .with_child(
549                    Label::new(active_layer.language.name().to_string(), style.text.clone())
550                        .contained()
551                        .with_margin_right(style.secondary_text_spacing),
552                )
553                .with_child(Label::new(
554                    format_node_range(active_layer.node()),
555                    style
556                        .secondary_text
557                        .clone()
558                        .unwrap_or_else(|| style.text.clone()),
559                ))
560                .contained()
561                .with_style(style.container)
562        })
563        .with_cursor_style(CursorStyle::PointingHand)
564        .on_click(MouseButton::Left, move |_, view, cx| {
565            view.toggle_menu(cx);
566        })
567    }
568
569    fn render_menu_item(
570        theme: &Arc<Theme>,
571        active_layer: &OwnedSyntaxLayerInfo,
572        layer: SyntaxLayerInfo,
573        layer_ix: usize,
574        cx: &mut ViewContext<Self>,
575    ) -> impl Element<Self> {
576        enum ActivateLayer {}
577        MouseEventHandler::<ActivateLayer, _>::new(layer_ix, cx, move |state, _| {
578            let is_selected = layer.node() == active_layer.node();
579            let style = theme
580                .toolbar_dropdown_menu
581                .item
582                .style_for(state, is_selected);
583            Flex::row()
584                .with_child(
585                    Label::new(layer.language.name().to_string(), style.text.clone())
586                        .contained()
587                        .with_margin_right(style.secondary_text_spacing),
588                )
589                .with_child(Label::new(
590                    format_node_range(layer.node()),
591                    style
592                        .secondary_text
593                        .clone()
594                        .unwrap_or_else(|| style.text.clone()),
595                ))
596                .contained()
597                .with_style(style.container)
598        })
599        .with_cursor_style(CursorStyle::PointingHand)
600        .on_click(MouseButton::Left, move |_, view, cx| {
601            view.select_layer(layer_ix, cx);
602        })
603    }
604}
605
606fn format_node_range(node: Node) -> String {
607    let start = node.start_position();
608    let end = node.end_position();
609    format!(
610        "[{}:{} - {}:{}]",
611        start.row + 1,
612        start.column + 1,
613        end.row + 1,
614        end.column + 1,
615    )
616}
617
618impl Entity for SyntaxTreeToolbarItemView {
619    type Event = ();
620}
621
622impl View for SyntaxTreeToolbarItemView {
623    fn ui_name() -> &'static str {
624        "SyntaxTreeToolbarItemView"
625    }
626
627    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
628        self.render_menu(cx)
629            .unwrap_or_else(|| Empty::new().into_any())
630    }
631}
632
633impl ToolbarItemView for SyntaxTreeToolbarItemView {
634    fn set_active_pane_item(
635        &mut self,
636        active_pane_item: Option<&dyn ItemHandle>,
637        cx: &mut ViewContext<Self>,
638    ) -> workspace::ToolbarItemLocation {
639        self.menu_open = false;
640        if let Some(item) = active_pane_item {
641            if let Some(view) = item.downcast::<SyntaxTreeView>() {
642                self.tree_view = Some(view.clone());
643                self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
644                return ToolbarItemLocation::PrimaryLeft {
645                    flex: Some((1., false)),
646                };
647            }
648        }
649        self.tree_view = None;
650        self.subscription = None;
651        ToolbarItemLocation::Hidden
652    }
653}