Improve the look of the syntax tree view UI

Max Brunsfeld created

Change summary

crates/language/src/buffer.rs                 |   4 
crates/language_tools/src/language_tools.rs   |   2 
crates/language_tools/src/lsp_log.rs          |  32 
crates/language_tools/src/syntax_tree_view.rs | 375 ++++++++++++++++++--
crates/theme/src/theme.rs                     |  21 
crates/zed/src/zed.rs                         |   3 
styles/src/styleTree/app.ts                   |   4 
styles/src/styleTree/toolbarDropdownMenu.ts   |   8 
8 files changed, 382 insertions(+), 67 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -2117,6 +2117,10 @@ impl BufferSnapshot {
         }
     }
 
+    pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayerInfo> + '_ {
+        self.syntax.layers_for_range(0..self.len(), &self.text)
+    }
+
     pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayerInfo> {
         let offset = position.to_offset(self);
         self.syntax

crates/language_tools/src/language_tools.rs 🔗

@@ -7,7 +7,7 @@ mod lsp_log_tests;
 use gpui::AppContext;
 
 pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView};
-pub use syntax_tree_view::SyntaxTreeView;
+pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
 
 pub fn init(cx: &mut AppContext) {
     lsp_log::init(cx);

crates/language_tools/src/lsp_log.rs 🔗

@@ -541,12 +541,7 @@ impl View for LspLogToolbarItemView {
         let theme = theme::current(cx).clone();
         let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() };
         let log_view = log_view.read(cx);
-
-        let menu_rows = self
-            .log_view
-            .as_ref()
-            .and_then(|view| view.read(cx).menu_items(cx))
-            .unwrap_or_default();
+        let menu_rows = log_view.menu_items(cx).unwrap_or_default();
 
         let current_server_id = log_view.current_server_id;
         let current_server = current_server_id.and_then(|current_server_id| {
@@ -583,7 +578,7 @@ impl View for LspLogToolbarItemView {
                                     )
                                 }))
                                 .contained()
-                                .with_style(theme.lsp_log_menu.container)
+                                .with_style(theme.toolbar_dropdown_menu.container)
                                 .constrained()
                                 .with_width(400.)
                                 .with_height(400.)
@@ -593,6 +588,7 @@ impl View for LspLogToolbarItemView {
                             cx.notify()
                         }),
                     )
+                    .with_hoverable(true)
                     .with_fit_mode(OverlayFitMode::SwitchAnchor)
                     .with_anchor_corner(AnchorCorner::TopLeft)
                     .with_z_index(999)
@@ -685,7 +681,7 @@ impl LspLogToolbarItemView {
                     )
                 })
                 .unwrap_or_else(|| "No server selected".into());
-            let style = theme.lsp_log_menu.header.style_for(state, false);
+            let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
             Label::new(label, style.text.clone())
                 .contained()
                 .with_style(style.container)
@@ -711,7 +707,7 @@ impl LspLogToolbarItemView {
 
         Flex::column()
             .with_child({
-                let style = &theme.lsp_log_menu.server;
+                let style = &theme.toolbar_dropdown_menu.section_header;
                 Label::new(
                     format!("{} ({})", name.0, worktree.read(cx).root_name()),
                     style.text.clone(),
@@ -719,16 +715,19 @@ impl LspLogToolbarItemView {
                 .contained()
                 .with_style(style.container)
                 .constrained()
-                .with_height(theme.lsp_log_menu.row_height)
+                .with_height(theme.toolbar_dropdown_menu.row_height)
             })
             .with_child(
                 MouseEventHandler::<ActivateLog, _>::new(id.0, cx, move |state, _| {
-                    let style = theme.lsp_log_menu.item.style_for(state, logs_selected);
+                    let style = theme
+                        .toolbar_dropdown_menu
+                        .item
+                        .style_for(state, logs_selected);
                     Label::new(SERVER_LOGS, style.text.clone())
                         .contained()
                         .with_style(style.container)
                         .constrained()
-                        .with_height(theme.lsp_log_menu.row_height)
+                        .with_height(theme.toolbar_dropdown_menu.row_height)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, view, cx| {
@@ -737,12 +736,15 @@ impl LspLogToolbarItemView {
             )
             .with_child(
                 MouseEventHandler::<ActivateRpcTrace, _>::new(id.0, cx, move |state, cx| {
-                    let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_selected);
+                    let style = theme
+                        .toolbar_dropdown_menu
+                        .item
+                        .style_for(state, rpc_trace_selected);
                     Flex::row()
                         .with_child(
                             Label::new(RPC_MESSAGES, style.text.clone())
                                 .constrained()
-                                .with_height(theme.lsp_log_menu.row_height),
+                                .with_height(theme.toolbar_dropdown_menu.row_height),
                         )
                         .with_child(
                             ui::checkbox_with_label::<Self, _, Self, _>(
@@ -761,7 +763,7 @@ impl LspLogToolbarItemView {
                         .contained()
                         .with_style(style.container)
                         .constrained()
-                        .with_height(theme.lsp_log_menu.row_height)
+                        .with_height(theme.toolbar_dropdown_menu.row_height)
                 })
                 .with_cursor_style(CursorStyle::PointingHand)
                 .on_click(MouseButton::Left, move |_, view, cx| {

crates/language_tools/src/syntax_tree_view.rs 🔗

@@ -1,17 +1,21 @@
 use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId};
 use gpui::{
     actions,
-    elements::{Empty, Label, MouseEventHandler, ScrollTarget, UniformList, UniformListState},
+    elements::{
+        AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode,
+        ParentElement, ScrollTarget, Stack, UniformList, UniformListState,
+    },
     fonts::TextStyle,
-    platform::MouseButton,
-    AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle,
+    platform::{CursorStyle, MouseButton},
+    AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle,
 };
-use language::{Buffer, OwnedSyntaxLayerInfo};
-use std::ops::Range;
-use theme::ThemeSettings;
+use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo};
+use std::{ops::Range, sync::Arc};
+use theme::{Theme, ThemeSettings};
+use tree_sitter::Node;
 use workspace::{
     item::{Item, ItemHandle},
-    Workspace,
+    ToolbarItemLocation, ToolbarItemView, Workspace,
 };
 
 actions!(log, [OpenSyntaxTreeView]);
@@ -19,13 +23,17 @@ actions!(log, [OpenSyntaxTreeView]);
 pub fn init(cx: &mut AppContext) {
     cx.add_action(
         move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| {
-            let syntax_tree_view = cx.add_view(|cx| SyntaxTreeView::new(workspace, cx));
+            let active_item = workspace.active_item(cx);
+            let workspace_handle = workspace.weak_handle();
+            let syntax_tree_view =
+                cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx));
             workspace.add_item(Box::new(syntax_tree_view), cx);
         },
     );
 }
 
 pub struct SyntaxTreeView {
+    workspace_handle: WeakViewHandle<Workspace>,
     editor: Option<EditorState>,
     mouse_y: Option<f32>,
     line_height: Option<f32>,
@@ -34,12 +42,19 @@ pub struct SyntaxTreeView {
     hovered_descendant_ix: Option<usize>,
 }
 
+pub struct SyntaxTreeToolbarItemView {
+    tree_view: Option<ViewHandle<SyntaxTreeView>>,
+    subscription: Option<gpui::Subscription>,
+    menu_open: bool,
+}
+
 struct EditorState {
     editor: ViewHandle<Editor>,
     active_buffer: Option<BufferState>,
     _subscription: gpui::Subscription,
 }
 
+#[derive(Clone)]
 struct BufferState {
     buffer: ModelHandle<Buffer>,
     excerpt_id: ExcerptId,
@@ -47,8 +62,13 @@ struct BufferState {
 }
 
 impl SyntaxTreeView {
-    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        workspace_handle: WeakViewHandle<Workspace>,
+        active_item: Option<Box<dyn ItemHandle>>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let mut this = Self {
+            workspace_handle: workspace_handle.clone(),
             list_state: UniformListState::default(),
             editor: None,
             mouse_y: None,
@@ -57,9 +77,9 @@ impl SyntaxTreeView {
             selected_descendant_ix: None,
         };
 
-        this.workspace_updated(workspace.active_item(cx), cx);
+        this.workspace_updated(active_item, cx);
         cx.observe(
-            &workspace.weak_handle().upgrade(cx).unwrap(),
+            &workspace_handle.upgrade(cx).unwrap(),
             |this, workspace, cx| {
                 this.workspace_updated(workspace.read(cx).active_item(cx), cx);
             },
@@ -94,12 +114,12 @@ impl SyntaxTreeView {
         }
 
         let subscription = cx.subscribe(&editor, |this, _, event, cx| {
-            let reset_layer = match event {
+            let did_reparse = match event {
                 editor::Event::Reparsed => true,
                 editor::Event::SelectionsChanged { .. } => false,
                 _ => return,
             };
-            this.editor_updated(reset_layer, cx);
+            this.editor_updated(did_reparse, cx);
         });
 
         self.editor = Some(EditorState {
@@ -110,7 +130,7 @@ impl SyntaxTreeView {
         self.editor_updated(true, cx);
     }
 
-    fn editor_updated(&mut self, reset_layer: bool, cx: &mut ViewContext<Self>) -> Option<()> {
+    fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext<Self>) -> Option<()> {
         // Find which excerpt the cursor is in, and the position within that excerpted buffer.
         let editor_state = self.editor.as_mut()?;
         let editor = &editor_state.editor.read(cx);
@@ -129,24 +149,39 @@ impl SyntaxTreeView {
                 excerpt_id,
                 active_layer: None,
             });
-        if reset_layer
-            || buffer_state.buffer != buffer
-            || buffer_state.excerpt_id != buffer_state.excerpt_id
-        {
+        let mut prev_layer = None;
+        if did_reparse {
+            prev_layer = buffer_state.active_layer.take();
+        }
+        if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id {
             buffer_state.buffer = buffer.clone();
             buffer_state.excerpt_id = excerpt_id;
             buffer_state.active_layer = None;
         }
 
-        // Within the active layer, find the syntax node under the cursor,
-        // and scroll to it.
         let layer = match &mut buffer_state.active_layer {
             Some(layer) => layer,
             None => {
-                let layer = buffer.read(cx).snapshot().syntax_layer_at(0)?.to_owned();
-                buffer_state.active_layer.insert(layer)
+                let snapshot = buffer.read(cx).snapshot();
+                let layer = if let Some(prev_layer) = prev_layer {
+                    let prev_range = prev_layer.node().byte_range();
+                    snapshot
+                        .syntax_layers()
+                        .filter(|layer| layer.language == &prev_layer.language)
+                        .min_by_key(|layer| {
+                            let range = layer.node().byte_range();
+                            ((range.start as i64) - (prev_range.start as i64)).abs()
+                                + ((range.end as i64) - (prev_range.end as i64)).abs()
+                        })?
+                } else {
+                    snapshot.syntax_layers().next()?
+                };
+                buffer_state.active_layer.insert(layer.to_owned())
             }
         };
+
+        // Within the active layer, find the syntax node under the cursor,
+        // and scroll to it.
         let mut cursor = layer.node().walk();
         while cursor.goto_first_child_for_byte(range.start).is_some() {
             if !range.is_empty() && cursor.node().end_byte() == range.start {
@@ -236,6 +271,55 @@ impl SyntaxTreeView {
         });
         Some(())
     }
+
+    fn render_node(
+        node: Node,
+        depth: u32,
+        selected: bool,
+        hovered: bool,
+        list_hovered: bool,
+        style: &TextStyle,
+        editor_theme: &theme::Editor,
+        cx: &AppContext,
+    ) -> gpui::AnyElement<SyntaxTreeView> {
+        let mut range_style = style.clone();
+        let mut anonymous_node_style = style.clone();
+        let em_width = style.em_width(cx.font_cache());
+        let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round();
+
+        range_style.color = editor_theme.line_number;
+
+        let string_color = editor_theme
+            .syntax
+            .highlights
+            .iter()
+            .find_map(|(name, style)| (name == "string").then(|| style.color)?);
+        if let Some(color) = string_color {
+            anonymous_node_style.color = color;
+        }
+
+        Flex::row()
+            .with_child(
+                if node.is_named() {
+                    Label::new(node.kind(), style.clone())
+                } else {
+                    Label::new(format!("\"{}\"", node.kind()), anonymous_node_style)
+                }
+                .contained()
+                .with_margin_right(em_width),
+            )
+            .with_child(Label::new(format_node_range(node), range_style))
+            .contained()
+            .with_background_color(if selected {
+                editor_theme.selection.selection
+            } else if hovered && list_hovered {
+                editor_theme.active_line_background
+            } else {
+                Default::default()
+            })
+            .with_padding_left(gutter_padding + depth as f32 * 18.0)
+            .into_any()
+    }
 }
 
 impl Entity for SyntaxTreeView {
@@ -269,9 +353,9 @@ impl View for SyntaxTreeView {
             underline: Default::default(),
         };
 
-        let line_height = Some(cx.font_cache().line_height(font_size));
-        if line_height != self.line_height {
-            self.line_height = line_height;
+        let line_height = cx.font_cache().line_height(font_size);
+        if Some(line_height) != self.line_height {
+            self.line_height = Some(line_height);
             self.hover_state_changed(cx);
         }
 
@@ -282,13 +366,14 @@ impl View for SyntaxTreeView {
             .and_then(|buffer| buffer.active_layer.as_ref())
         {
             let layer = layer.clone();
+            let theme = editor_theme.clone();
             return MouseEventHandler::<Self, Self>::new(0, cx, move |state, cx| {
                 let list_hovered = state.hovered();
                 UniformList::new(
                     self.list_state.clone(),
                     layer.node().descendant_count(),
                     cx,
-                    move |this, range, items, _| {
+                    move |this, range, items, cx| {
                         let mut cursor = layer.node().walk();
                         let mut descendant_ix = range.start as usize;
                         cursor.goto_descendant(descendant_ix);
@@ -304,22 +389,16 @@ impl View for SyntaxTreeView {
                                     break;
                                 }
                             } else {
-                                let node = cursor.node();
-                                let hovered = Some(descendant_ix) == this.hovered_descendant_ix;
-                                let selected = Some(descendant_ix) == this.selected_descendant_ix;
-                                items.push(
-                                    Label::new(node.kind(), style.clone())
-                                        .contained()
-                                        .with_background_color(if selected {
-                                            editor_theme.selection.selection
-                                        } else if hovered && list_hovered {
-                                            editor_theme.active_line_background
-                                        } else {
-                                            Default::default()
-                                        })
-                                        .with_padding_left(depth as f32 * 18.0)
-                                        .into_any(),
-                                );
+                                items.push(Self::render_node(
+                                    cursor.node(),
+                                    depth,
+                                    Some(descendant_ix) == this.selected_descendant_ix,
+                                    Some(descendant_ix) == this.hovered_descendant_ix,
+                                    list_hovered,
+                                    &style,
+                                    &theme,
+                                    cx,
+                                ));
                                 descendant_ix += 1;
                                 if cursor.goto_first_child() {
                                     depth += 1;
@@ -358,4 +437,216 @@ impl Item for SyntaxTreeView {
     ) -> gpui::AnyElement<V> {
         Label::new("Syntax Tree", style.label.clone()).into_any()
     }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: workspace::WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Self>
+    where
+        Self: Sized,
+    {
+        let mut clone = Self::new(self.workspace_handle.clone(), None, cx);
+        if let Some(editor) = &self.editor {
+            clone.set_editor(editor.editor.clone(), cx)
+        }
+        Some(clone)
+    }
+}
+
+impl SyntaxTreeToolbarItemView {
+    pub fn new() -> Self {
+        Self {
+            menu_open: false,
+            tree_view: None,
+            subscription: None,
+        }
+    }
+
+    fn render_menu(
+        &mut self,
+        cx: &mut ViewContext<'_, '_, Self>,
+    ) -> Option<gpui::AnyElement<Self>> {
+        let theme = theme::current(cx).clone();
+        let tree_view = self.tree_view.as_ref()?;
+        let tree_view = tree_view.read(cx);
+
+        let editor_state = tree_view.editor.as_ref()?;
+        let buffer_state = editor_state.active_buffer.as_ref()?;
+        let active_layer = buffer_state.active_layer.clone()?;
+        let active_buffer = buffer_state.buffer.read(cx).snapshot();
+
+        enum Menu {}
+
+        Some(
+            Stack::new()
+                .with_child(Self::render_header(&theme, &active_layer, cx))
+                .with_children(self.menu_open.then(|| {
+                    Overlay::new(
+                        MouseEventHandler::<Menu, _>::new(0, cx, move |_, cx| {
+                            Flex::column()
+                                .with_children(active_buffer.syntax_layers().enumerate().map(
+                                    |(ix, layer)| {
+                                        Self::render_menu_item(&theme, &active_layer, layer, ix, cx)
+                                    },
+                                ))
+                                .contained()
+                                .with_style(theme.toolbar_dropdown_menu.container)
+                                .constrained()
+                                .with_width(400.)
+                                .with_height(400.)
+                        })
+                        .on_down_out(MouseButton::Left, |_, this, cx| {
+                            this.menu_open = false;
+                            cx.notify()
+                        }),
+                    )
+                    .with_hoverable(true)
+                    .with_fit_mode(OverlayFitMode::SwitchAnchor)
+                    .with_anchor_corner(AnchorCorner::TopLeft)
+                    .with_z_index(999)
+                    .aligned()
+                    .bottom()
+                    .left()
+                }))
+                .aligned()
+                .left()
+                .clipped()
+                .into_any(),
+        )
+    }
+
+    fn toggle_menu(&mut self, cx: &mut ViewContext<Self>) {
+        self.menu_open = !self.menu_open;
+        cx.notify();
+    }
+
+    fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> Option<()> {
+        let tree_view = self.tree_view.as_ref()?;
+        tree_view.update(cx, |view, cx| {
+            let editor_state = view.editor.as_mut()?;
+            let buffer_state = editor_state.active_buffer.as_mut()?;
+            let snapshot = buffer_state.buffer.read(cx).snapshot();
+            let layer = snapshot.syntax_layers().nth(layer_ix)?;
+            buffer_state.active_layer = Some(layer.to_owned());
+            view.selected_descendant_ix = None;
+            cx.notify();
+            Some(())
+        })
+    }
+
+    fn render_header(
+        theme: &Arc<Theme>,
+        active_layer: &OwnedSyntaxLayerInfo,
+        cx: &mut ViewContext<Self>,
+    ) -> impl Element<Self> {
+        enum ToggleMenu {}
+        MouseEventHandler::<ToggleMenu, Self>::new(0, cx, move |state, _| {
+            let style = theme.toolbar_dropdown_menu.header.style_for(state, false);
+            Flex::row()
+                .with_child(
+                    Label::new(active_layer.language.name().to_string(), style.text.clone())
+                        .contained()
+                        .with_margin_right(style.secondary_text_spacing),
+                )
+                .with_child(Label::new(
+                    format_node_range(active_layer.node()),
+                    style
+                        .secondary_text
+                        .clone()
+                        .unwrap_or_else(|| style.text.clone()),
+                ))
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, view, cx| {
+            view.toggle_menu(cx);
+        })
+    }
+
+    fn render_menu_item(
+        theme: &Arc<Theme>,
+        active_layer: &OwnedSyntaxLayerInfo,
+        layer: SyntaxLayerInfo,
+        layer_ix: usize,
+        cx: &mut ViewContext<Self>,
+    ) -> impl Element<Self> {
+        enum ActivateLayer {}
+        MouseEventHandler::<ActivateLayer, _>::new(layer_ix, cx, move |state, _| {
+            let is_selected = layer.node() == active_layer.node();
+            let style = theme
+                .toolbar_dropdown_menu
+                .item
+                .style_for(state, is_selected);
+            Flex::row()
+                .with_child(
+                    Label::new(layer.language.name().to_string(), style.text.clone())
+                        .contained()
+                        .with_margin_right(style.secondary_text_spacing),
+                )
+                .with_child(Label::new(
+                    format_node_range(layer.node()),
+                    style
+                        .secondary_text
+                        .clone()
+                        .unwrap_or_else(|| style.text.clone()),
+                ))
+                .contained()
+                .with_style(style.container)
+        })
+        .with_cursor_style(CursorStyle::PointingHand)
+        .on_click(MouseButton::Left, move |_, view, cx| {
+            view.select_layer(layer_ix, cx);
+        })
+    }
+}
+
+fn format_node_range(node: Node) -> String {
+    let start = node.start_position();
+    let end = node.end_position();
+    format!(
+        "[{}:{} - {}:{}]",
+        start.row + 1,
+        start.column + 1,
+        end.row + 1,
+        end.column + 1,
+    )
+}
+
+impl Entity for SyntaxTreeToolbarItemView {
+    type Event = ();
+}
+
+impl View for SyntaxTreeToolbarItemView {
+    fn ui_name() -> &'static str {
+        "SyntaxTreeToolbarItemView"
+    }
+
+    fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+        self.render_menu(cx)
+            .unwrap_or_else(|| Empty::new().into_any())
+    }
+}
+
+impl ToolbarItemView for SyntaxTreeToolbarItemView {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> workspace::ToolbarItemLocation {
+        self.menu_open = false;
+        if let Some(item) = active_pane_item {
+            if let Some(view) = item.downcast::<SyntaxTreeView>() {
+                self.tree_view = Some(view.clone());
+                self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify()));
+                return ToolbarItemLocation::PrimaryLeft {
+                    flex: Some((1., false)),
+                };
+            }
+        }
+        self.tree_view = None;
+        self.subscription = None;
+        ToolbarItemLocation::Hidden
+    }
 }

crates/theme/src/theme.rs 🔗

@@ -44,7 +44,7 @@ pub struct Theme {
     pub context_menu: ContextMenu,
     pub contacts_popover: ContactsPopover,
     pub contact_list: ContactList,
-    pub lsp_log_menu: LspLogMenu,
+    pub toolbar_dropdown_menu: DropdownMenu,
     pub copilot: Copilot,
     pub contact_finder: ContactFinder,
     pub project_panel: ProjectPanel,
@@ -246,15 +246,26 @@ pub struct ContactFinder {
 }
 
 #[derive(Deserialize, Default)]
-pub struct LspLogMenu {
+pub struct DropdownMenu {
     #[serde(flatten)]
     pub container: ContainerStyle,
-    pub header: Interactive<ContainedText>,
-    pub server: ContainedText,
-    pub item: Interactive<ContainedText>,
+    pub header: Interactive<DropdownMenuItem>,
+    pub section_header: ContainedText,
+    pub item: Interactive<DropdownMenuItem>,
     pub row_height: f32,
 }
 
+#[derive(Deserialize, Default)]
+pub struct DropdownMenuItem {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    #[serde(flatten)]
+    pub text: TextStyle,
+    pub secondary_text: Option<TextStyle>,
+    #[serde(default)]
+    pub secondary_text_spacing: f32,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct TabBar {
     #[serde(flatten)]

crates/zed/src/zed.rs 🔗

@@ -314,6 +314,9 @@ pub fn initialize_workspace(
                                 let lsp_log_item =
                                     cx.add_view(|_| language_tools::LspLogToolbarItemView::new());
                                 toolbar.add_item(lsp_log_item, cx);
+                                let syntax_tree_item = cx
+                                    .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
+                                toolbar.add_item(syntax_tree_item, cx);
                             })
                         });
                     }

styles/src/styleTree/app.ts 🔗

@@ -17,7 +17,7 @@ import projectSharedNotification from "./projectSharedNotification"
 import tooltip from "./tooltip"
 import terminal from "./terminal"
 import contactList from "./contactList"
-import lspLogMenu from "./lspLogMenu"
+import toolbarDropdownMenu from "./toolbarDropdownMenu"
 import incomingCallNotification from "./incomingCallNotification"
 import { ColorScheme } from "../theme/colorScheme"
 import feedback from "./feedback"
@@ -46,7 +46,7 @@ export default function app(colorScheme: ColorScheme): Object {
         contactsPopover: contactsPopover(colorScheme),
         contactFinder: contactFinder(colorScheme),
         contactList: contactList(colorScheme),
-        lspLogMenu: lspLogMenu(colorScheme),
+        toolbarDropdownMenu: toolbarDropdownMenu(colorScheme),
         search: search(colorScheme),
         sharedScreen: sharedScreen(colorScheme),
         updateNotification: updateNotification(colorScheme),

styles/src/styleTree/lspLogMenu.ts → styles/src/styleTree/toolbarDropdownMenu.ts 🔗

@@ -1,7 +1,7 @@
 import { ColorScheme } from "../theme/colorScheme"
 import { background, border, text } from "./components"
 
-export default function contactsPanel(colorScheme: ColorScheme) {
+export default function dropdownMenu(colorScheme: ColorScheme) {
     let layer = colorScheme.middle
 
     return {
@@ -11,6 +11,8 @@ export default function contactsPanel(colorScheme: ColorScheme) {
         shadow: colorScheme.popoverShadow,
         header: {
             ...text(layer, "sans", { size: "sm" }),
+            secondaryText: text(layer, "sans", { size: "sm", color: "#aaaaaa" }),
+            secondaryTextSpacing: 10,
             padding: { left: 8, right: 8, top: 2, bottom: 2 },
             cornerRadius: 6,
             background: background(layer, "on"),
@@ -20,12 +22,14 @@ export default function contactsPanel(colorScheme: ColorScheme) {
                 ...text(layer, "sans", "hovered", { size: "sm" }),
             }
         },
-        server: {
+        sectionHeader: {
             ...text(layer, "sans", { size: "sm" }),
             padding: { left: 8, right: 8, top: 8, bottom: 8 },
         },
         item: {
             ...text(layer, "sans", { size: "sm" }),
+            secondaryTextSpacing: 10,
+            secondaryText: text(layer, "sans", { size: "sm" }),
             padding: { left: 18, right: 18, top: 2, bottom: 2 },
             hover: {
                 background: background(layer, "hovered"),