syntax_tree_view.rs

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