syntax_tree_view.rs

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