diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d9efb0fbc6..5041ab759d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2117,6 +2117,10 @@ impl BufferSnapshot { } } + pub fn syntax_layers(&self) -> impl Iterator + '_ { + self.syntax.layers_for_range(0..self.len(), &self.text) + } + pub fn syntax_layer_at(&self, position: D) -> Option { let offset = position.to_offset(self); self.syntax diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs index b61c8d3c80..0a1f31f03f 100644 --- a/crates/language_tools/src/language_tools.rs +++ b/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); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index cec690d4a6..b9036fa6d2 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/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::::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::::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::( @@ -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| { diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 781961af07..3f9b087f70 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/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, editor: Option, mouse_y: Option, line_height: Option, @@ -34,12 +42,19 @@ pub struct SyntaxTreeView { hovered_descendant_ix: Option, } +pub struct SyntaxTreeToolbarItemView { + tree_view: Option>, + subscription: Option, + menu_open: bool, +} + struct EditorState { editor: ViewHandle, active_buffer: Option, _subscription: gpui::Subscription, } +#[derive(Clone)] struct BufferState { buffer: ModelHandle, excerpt_id: ExcerptId, @@ -47,8 +62,13 @@ struct BufferState { } impl SyntaxTreeView { - pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + pub fn new( + workspace_handle: WeakViewHandle, + active_item: Option>, + cx: &mut ViewContext, + ) -> 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) -> Option<()> { + fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext) -> 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 { + 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::::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 { Label::new("Syntax Tree", style.label.clone()).into_any() } + + fn clone_on_split( + &self, + _workspace_id: workspace::WorkspaceId, + cx: &mut ViewContext, + ) -> Option + 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> { + 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::::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.menu_open = !self.menu_open; + cx.notify(); + } + + fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext) -> 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, + active_layer: &OwnedSyntaxLayerInfo, + cx: &mut ViewContext, + ) -> impl Element { + enum ToggleMenu {} + MouseEventHandler::::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, + active_layer: &OwnedSyntaxLayerInfo, + layer: SyntaxLayerInfo, + layer_ix: usize, + cx: &mut ViewContext, + ) -> impl Element { + enum ActivateLayer {} + MouseEventHandler::::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.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, + ) -> workspace::ToolbarItemLocation { + self.menu_open = false; + if let Some(item) = active_pane_item { + if let Some(view) = item.downcast::() { + 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 + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9bd17910d2..c7563ec87a 100644 --- a/crates/theme/src/theme.rs +++ b/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, - pub server: ContainedText, - pub item: Interactive, + pub header: Interactive, + pub section_header: ContainedText, + pub item: Interactive, pub row_height: f32, } +#[derive(Deserialize, Default)] +pub struct DropdownMenuItem { + #[serde(flatten)] + pub container: ContainerStyle, + #[serde(flatten)] + pub text: TextStyle, + pub secondary_text: Option, + #[serde(default)] + pub secondary_text_spacing: f32, +} + #[derive(Clone, Deserialize, Default)] pub struct TabBar { #[serde(flatten)] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cef6cc7d3c..b0b88b37ac 100644 --- a/crates/zed/src/zed.rs +++ b/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); }) }); } diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 886553d418..6244cbae10 100644 --- a/styles/src/styleTree/app.ts +++ b/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), diff --git a/styles/src/styleTree/lspLogMenu.ts b/styles/src/styleTree/toolbarDropdownMenu.ts similarity index 81% rename from styles/src/styleTree/lspLogMenu.ts rename to styles/src/styleTree/toolbarDropdownMenu.ts index 94dd4831b2..92616eb022 100644 --- a/styles/src/styleTree/lspLogMenu.ts +++ b/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"),