use editor::{ Anchor, Editor, HighlightKey, MultiBufferSnapshot, SelectionEffects, ToPoint, scroll::Autoscroll, }; use gpui::{ Action, App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable, HighlightStyle, Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Render, ScrollStrategy, SharedString, Styled, Task, UniformListScrollHandle, WeakEntity, Window, actions, div, rems, uniform_list, }; use language::{BufferId, Point, ToOffset}; use menu::{SelectNext, SelectPrevious}; use std::{mem, ops::Range, sync::Arc, time::Duration}; use theme::ActiveTheme; use theme::SyntaxTheme; use ui::{ ButtonCommon, ButtonLike, ButtonStyle, Color, ContextMenu, FluentBuilder as _, IconButton, IconName, IconPosition, IconSize, Label, LabelCommon, LabelSize, PopoverMenu, PopoverMenuHandle, StyledExt, Toggleable, Tooltip, WithScrollbar, h_flex, v_flex, }; use workspace::{ Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::{Item, ItemHandle}, }; actions!( dev, [ /// Opens the highlights tree view for the current file. OpenHighlightsTreeView, ] ); actions!( highlights_tree_view, [ /// Toggles showing text highlights. ToggleTextHighlights, /// Toggles showing semantic token highlights. ToggleSemanticTokens, /// Toggles showing syntax token highlights. ToggleSyntaxTokens, ] ); pub fn init(cx: &mut App) { cx.observe_new(move |workspace: &mut Workspace, _, _| { workspace.register_action(move |workspace, _: &OpenHighlightsTreeView, window, cx| { let active_item = workspace.active_item(cx); let workspace_handle = workspace.weak_handle(); let highlights_tree_view = cx.new(|cx| HighlightsTreeView::new(workspace_handle, active_item, window, cx)); workspace.split_item( SplitDirection::Right, Box::new(highlights_tree_view), window, cx, ) }); }) .detach(); } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum HighlightCategory { Text(HighlightKey), SyntaxToken { capture_name: SharedString, theme_key: Option, }, SemanticToken { token_type: Option, token_modifiers: Option, theme_key: Option, }, } impl HighlightCategory { fn label(&self) -> SharedString { match self { HighlightCategory::Text(key) => format!("text: {key:?}").into(), HighlightCategory::SyntaxToken { capture_name, theme_key: Some(theme_key), } => format!("syntax: {capture_name} \u{2192} {theme_key}").into(), HighlightCategory::SyntaxToken { capture_name, theme_key: None, } => format!("syntax: {capture_name}").into(), HighlightCategory::SemanticToken { token_type, token_modifiers, theme_key, } => { let label = match (token_type, token_modifiers) { (Some(token_type), Some(modifiers)) => { format!("semantic token: {token_type} [{modifiers}]") } (Some(token_type), None) => format!("semantic token: {token_type}"), (None, Some(modifiers)) => format!("semantic token [{modifiers}]"), (None, None) => "semantic token".to_string(), }; if let Some(theme_key) = theme_key { format!("{label} \u{2192} {theme_key}").into() } else { label.into() } } } } } #[derive(Debug, Clone)] struct HighlightEntry { range: Range, buffer_id: BufferId, buffer_point_range: Range, range_display: SharedString, style: HighlightStyle, category: HighlightCategory, } /// An item in the display list: either a separator between excerpts or a highlight entry. #[derive(Debug, Clone)] enum DisplayItem { ExcerptSeparator { label: SharedString, }, Entry { /// Index into `cached_entries`. entry_ix: usize, }, } pub struct HighlightsTreeView { workspace_handle: WeakEntity, editor: Option, list_scroll_handle: UniformListScrollHandle, selected_item_ix: Option, hovered_item_ix: Option, focus_handle: FocusHandle, cached_entries: Vec, display_items: Vec, is_singleton: bool, show_text_highlights: bool, show_syntax_tokens: bool, show_semantic_tokens: bool, skip_next_scroll: bool, refresh_task: Task<()>, } pub struct HighlightsTreeToolbarItemView { tree_view: Option>, _subscription: Option, toggle_settings_handle: PopoverMenuHandle, } struct EditorState { editor: Entity, _subscription: gpui::Subscription, } struct SemanticHighlightEntry { range: Range, style: HighlightStyle, category: HighlightCategory, } struct HighlightRefreshInput { multi_buffer_snapshot: MultiBufferSnapshot, text_highlights: Vec<(HighlightKey, Arc<(HighlightStyle, Vec>)>)>, semantic_highlights: Vec, syntax_theme: Arc, } impl HighlightsTreeView { pub fn new( workspace_handle: WeakEntity, active_item: Option>, window: &mut Window, cx: &mut Context, ) -> Self { let mut this = Self { workspace_handle: workspace_handle.clone(), list_scroll_handle: UniformListScrollHandle::new(), editor: None, hovered_item_ix: None, selected_item_ix: None, focus_handle: cx.focus_handle(), cached_entries: Vec::new(), display_items: Vec::new(), is_singleton: true, show_text_highlights: true, show_syntax_tokens: true, show_semantic_tokens: true, skip_next_scroll: false, refresh_task: Task::ready(()), }; this.handle_item_updated(active_item, window, cx); cx.subscribe_in( &workspace_handle.upgrade().unwrap(), window, move |this, workspace, event, window, cx| match event { WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => { this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx) } WorkspaceEvent::ItemRemoved { item_id } => { this.handle_item_removed(item_id, window, cx); } _ => {} }, ) .detach(); this } fn handle_item_updated( &mut self, active_item: Option>, window: &mut Window, cx: &mut Context, ) { let active_editor = match active_item { Some(active_item) => { if active_item.item_id() == cx.entity_id() { return; } else { match active_item.downcast::() { Some(active_editor) => active_editor, None => { self.clear(cx); return; } } } } None => { self.clear(cx); return; } }; let is_different_editor = self .editor .as_ref() .is_none_or(|state| state.editor != active_editor); if is_different_editor { self.set_editor(active_editor, window, cx); } } fn handle_item_removed( &mut self, item_id: &EntityId, _window: &mut Window, cx: &mut Context, ) { if self .editor .as_ref() .is_some_and(|state| state.editor.entity_id() == *item_id) { self.clear(cx); } } fn clear(&mut self, cx: &mut Context) { self.refresh_task = Task::ready(()); self.cached_entries.clear(); self.display_items.clear(); self.selected_item_ix = None; self.hovered_item_ix = None; if let Some(state) = self.editor.take() { Self::clear_editor_highlights(&state.editor, cx); } cx.notify(); } fn set_editor(&mut self, editor: Entity, window: &mut Window, cx: &mut Context) { if let Some(state) = &self.editor { if state.editor == editor { return; } Self::clear_editor_highlights(&state.editor, cx); } let subscription = cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event { editor::EditorEvent::Reparsed(_) => this.schedule_refresh_highlights(window, cx), editor::EditorEvent::SelectionsChanged { .. } => { this.update_selection_from_editor(cx); } _ => return, }); self.editor = Some(EditorState { editor, _subscription: subscription, }); self.refresh_task = Task::ready(()); self.cached_entries.clear(); self.display_items.clear(); self.selected_item_ix = None; self.hovered_item_ix = None; self.schedule_refresh_highlights(window, cx); cx.notify(); } fn schedule_refresh_highlights(&mut self, window: &mut Window, cx: &mut Context) { self.refresh_task = cx.spawn_in(window, async move |this, cx| { cx.background_executor() .timer(Duration::from_millis(30)) .await; let Some(input) = this .update(cx, |this, cx| this.highlight_refresh_input(cx)) .ok() .flatten() else { return; }; let new_highlights = cx .background_spawn(async move { build_highlight_entries(input) }) .await; this.update_in(cx, |this, _window, cx| { this.apply_highlight_refresh(new_highlights, cx); }) .ok(); }); } fn highlight_refresh_input(&self, cx: &mut Context) -> Option { let editor_state = self.editor.as_ref()?; let editor = editor_state.editor.read(cx); let display_map = editor.display_map.clone(); let project = editor.project().cloned()?; let multi_buffer = editor.buffer().clone(); let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); let syntax_theme = cx.theme().syntax().clone(); let (text_highlights, semantic_token_highlights) = display_map.update(cx, |display_map, _| { let text_highlights = display_map .all_text_highlights() .map(|(key, highlights)| (*key, highlights.clone())) .collect::>(); let semantic_token_highlights = display_map .all_semantic_token_highlights() .map(|(buffer_id, (tokens, interner))| { (*buffer_id, tokens.clone(), interner.clone()) }) .collect::>(); (text_highlights, semantic_token_highlights) }); let mut semantic_highlights = Vec::new(); project.read(cx).lsp_store().update(cx, |lsp_store, cx| { for (buffer_id, tokens, interner) in semantic_token_highlights { let language_name = multi_buffer .read(cx) .buffer(buffer_id) .and_then(|buffer| buffer.read(cx).language().map(|language| language.name())); for token in tokens.iter() { let Some(stylizer) = lsp_store.get_or_create_token_stylizer( token.server_id, language_name.as_ref(), cx, ) else { continue; }; let theme_key = stylizer .rules_for_token(token.token_type) .and_then(|rules| { rules .iter() .filter(|rule| { rule.token_modifiers.iter().all(|modifier| { stylizer.has_modifier(token.token_modifiers, modifier) }) }) .fold(None, |theme_key, rule| { rule.style .iter() .find(|style_name| { syntax_theme.style_for_name(style_name).is_some() }) .map(|style_name| SharedString::from(style_name.clone())) .or(theme_key) }) }); semantic_highlights.push(SemanticHighlightEntry { range: token.range.start..token.range.end, style: interner[token.style], category: HighlightCategory::SemanticToken { token_type: stylizer.token_type_name(token.token_type).cloned(), token_modifiers: stylizer .token_modifiers(token.token_modifiers) .map(SharedString::from), theme_key, }, }); } } }); Some(HighlightRefreshInput { multi_buffer_snapshot, text_highlights, semantic_highlights, syntax_theme, }) } fn apply_highlight_refresh( &mut self, new_highlights: Vec, cx: &mut Context, ) { let Some(editor) = self.editor.as_ref().map(|state| state.editor.clone()) else { return; }; let editor = editor.read(cx); let cursor_position = editor.selections.newest_anchor().head(); let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); self.is_singleton = multi_buffer_snapshot.is_singleton(); self.cached_entries = new_highlights; self.hovered_item_ix = None; self.rebuild_display_items(&multi_buffer_snapshot, cx); let should_scroll = !mem::take(&mut self.skip_next_scroll); self.update_selection_for_cursor(&cursor_position, &multi_buffer_snapshot, should_scroll); self.sync_editor_highlight_for_selected_entry(cx); cx.notify(); } fn rebuild_display_items(&mut self, snapshot: &MultiBufferSnapshot, cx: &App) { self.display_items.clear(); let mut last_range_end = None; for (entry_ix, entry) in self.cached_entries.iter().enumerate() { if !self.should_show_entry(entry) { continue; } if !self.is_singleton { let excerpt_changed = last_range_end.is_none_or(|anchor| { snapshot .excerpt_containing(anchor..entry.range.start) .is_none() }); if excerpt_changed { last_range_end = Some(entry.range.end); let label = excerpt_label_for(entry, snapshot, cx); self.display_items .push(DisplayItem::ExcerptSeparator { label }); } } self.display_items.push(DisplayItem::Entry { entry_ix }); } } fn should_show_entry(&self, entry: &HighlightEntry) -> bool { match entry.category { HighlightCategory::Text(_) => self.show_text_highlights, HighlightCategory::SyntaxToken { .. } => self.show_syntax_tokens, HighlightCategory::SemanticToken { .. } => self.show_semantic_tokens, } } fn update_selection_from_editor(&mut self, cx: &mut Context) { let Some(editor_state) = self.editor.as_ref() else { return; }; let (cursor_position, multi_buffer_snapshot) = { let editor = editor_state.editor.read(cx); ( editor.selections.newest_anchor().head(), editor.buffer().read(cx).snapshot(cx), ) }; let should_update_selection = !mem::take(&mut self.skip_next_scroll); if !should_update_selection { self.sync_editor_highlight_for_selected_entry(cx); return; } let should_notify = self.update_selection_for_cursor(&cursor_position, &multi_buffer_snapshot, true); self.sync_editor_highlight_for_selected_entry(cx); if should_notify { cx.notify(); } } fn update_selection_for_cursor( &mut self, cursor: &Anchor, snapshot: &MultiBufferSnapshot, should_scroll: bool, ) -> bool { let cursor_point = cursor.to_point(snapshot); let Some((cursor_buffer, cursor_point)) = snapshot.point_to_buffer_point(cursor_point) else { let changed = self.selected_item_ix.take().is_some(); return changed; }; let cursor_buffer_id = cursor_buffer.remote_id(); let best = self .display_items .iter() .enumerate() .filter_map(|(display_ix, item)| match item { DisplayItem::Entry { entry_ix } => { let entry = &self.cached_entries[*entry_ix]; Some((display_ix, *entry_ix, entry)) } _ => None, }) .filter(|(_, _, entry)| { entry.buffer_id == cursor_buffer_id && entry.buffer_point_range.start <= cursor_point && cursor_point < entry.buffer_point_range.end }) .min_by_key(|(_, _, entry)| { ( entry .buffer_point_range .end .row .saturating_sub(entry.buffer_point_range.start.row), entry .buffer_point_range .end .column .saturating_sub(entry.buffer_point_range.start.column), ) }) .map(|(display_ix, entry_ix, _)| (display_ix, entry_ix)); let selected_item_ix = best.map(|(_, entry_ix)| entry_ix); let changed = self.selected_item_ix != selected_item_ix; self.selected_item_ix = selected_item_ix; if should_scroll && let Some((display_ix, _)) = best { self.list_scroll_handle .scroll_to_item(display_ix, ScrollStrategy::Center); return true; } changed } fn update_editor_with_range_for_entry( &self, entry_ix: usize, window: &mut Window, cx: &mut Context, f: &mut dyn FnMut(&mut Editor, Range, usize, &mut Window, &mut Context), ) -> Option<()> { let editor_state = self.editor.as_ref()?; let entry = self.cached_entries.get(entry_ix)?; let range = entry.range.clone(); let key = cx.entity_id().as_u64() as usize; editor_state.editor.update(cx, |editor, cx| { f(editor, range, key, window, cx); }); Some(()) } fn render_entry(&self, entry: &HighlightEntry, selected: bool, cx: &App) -> Div { let colors = cx.theme().colors(); let style_preview = render_style_preview(entry.style, selected, cx); h_flex() .gap_1() .child(style_preview) .child(Label::new(entry.range_display.clone()).color(Color::Default)) .child( Label::new(entry.category.label()) .size(LabelSize::Small) .color(Color::Muted), ) .text_bg(if selected { colors.element_selected } else { Hsla::default() }) .pl(rems(0.5)) .hover(|style| style.bg(colors.element_hover)) } fn render_separator(&self, label: &SharedString, cx: &App) -> Div { let colors = cx.theme().colors(); h_flex() .gap_1() .px(rems(0.5)) .bg(colors.surface_background) .border_b_1() .border_color(colors.border_variant) .child( Label::new(label.clone()) .size(LabelSize::Small) .color(Color::Muted), ) } fn compute_items( &mut self, visible_range: Range, _window: &mut Window, cx: &mut Context, ) -> Vec
{ let mut items = Vec::new(); for display_ix in visible_range { let Some(display_item) = self.display_items.get(display_ix) else { continue; }; match display_item { DisplayItem::ExcerptSeparator { label } => { items.push(self.render_separator(label, cx)); } DisplayItem::Entry { entry_ix } => { let entry_ix = *entry_ix; let entry = &self.cached_entries[entry_ix]; let selected = Some(entry_ix) == self.selected_item_ix; let rendered = self .render_entry(entry, selected, cx) .on_mouse_down( MouseButton::Left, cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| { tree_view.selected_item_ix = Some(entry_ix); tree_view.skip_next_scroll = true; tree_view.update_editor_with_range_for_entry( entry_ix, window, cx, &mut |editor, mut range, _, window, cx| { mem::swap(&mut range.start, &mut range.end); editor.change_selections( SelectionEffects::scroll(Autoscroll::newest()), window, cx, |selections| { selections.select_ranges([range]); }, ); }, ); cx.notify(); }), ) .on_mouse_move(cx.listener( move |tree_view, _: &MouseMoveEvent, window, cx| { if tree_view.hovered_item_ix != Some(entry_ix) { tree_view.hovered_item_ix = Some(entry_ix); tree_view.update_editor_with_range_for_entry( entry_ix, window, cx, &mut |editor, range, key, _, cx| { Self::set_editor_highlights(editor, key, &[range], cx); }, ); cx.notify(); } }, )); items.push(rendered); } } } items } fn set_editor_highlights( editor: &mut Editor, key: usize, ranges: &[Range], cx: &mut Context, ) { editor.highlight_background( HighlightKey::HighlightsTreeView(key), ranges, |_, theme| theme.colors().editor_document_highlight_write_background, cx, ); } fn sync_editor_highlight_for_selected_entry(&self, cx: &mut Context) { let Some(editor_state) = self.editor.as_ref() else { return; }; let key = cx.entity_id().as_u64() as usize; let range = self .selected_item_ix .and_then(|entry_ix| self.cached_entries.get(entry_ix)) .map(|entry| entry.range.clone()); editor_state.editor.update(cx, |editor, cx| { if let Some(range) = range { Self::set_editor_highlights(editor, key, &[range], cx); } else { editor.clear_background_highlights(HighlightKey::HighlightsTreeView(key), cx); } }); } fn clear_editor_highlights(editor: &Entity, cx: &mut Context) { let highlight_key = HighlightKey::HighlightsTreeView(cx.entity_id().as_u64() as usize); editor.update(cx, |editor, cx| { editor.clear_background_highlights(highlight_key, cx); }); } fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { self.move_selection(-1, window, cx); } fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { self.move_selection(1, window, cx); } fn move_selection(&mut self, delta: i32, window: &mut Window, cx: &mut Context) { if self.display_items.is_empty() { return; } let entry_display_items: Vec<(usize, usize)> = self .display_items .iter() .enumerate() .filter_map(|(display_ix, item)| match item { DisplayItem::Entry { entry_ix } => Some((display_ix, *entry_ix)), _ => None, }) .collect(); if entry_display_items.is_empty() { return; } let current_pos = self .selected_item_ix .and_then(|selected| { entry_display_items .iter() .position(|(_, entry_ix)| *entry_ix == selected) }) .unwrap_or(0); let new_pos = if delta < 0 { current_pos.saturating_sub((-delta) as usize) } else { (current_pos + delta as usize).min(entry_display_items.len() - 1) }; if let Some(&(display_ix, entry_ix)) = entry_display_items.get(new_pos) { self.selected_item_ix = Some(entry_ix); self.skip_next_scroll = true; self.list_scroll_handle .scroll_to_item(display_ix, ScrollStrategy::Center); self.update_editor_with_range_for_entry( entry_ix, window, cx, &mut |editor, mut range, _, window, cx| { mem::swap(&mut range.start, &mut range.end); editor.change_selections( SelectionEffects::scroll(Autoscroll::newest()), window, cx, |selections| { selections.select_ranges([range]); }, ); }, ); cx.notify(); } } fn entry_count(&self) -> usize { self.cached_entries .iter() .filter(|entry| self.should_show_entry(entry)) .count() } } impl Render for HighlightsTreeView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let display_count = self.display_items.len(); div() .flex_1() .track_focus(&self.focus_handle) .key_context("HighlightsTreeView") .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_next)) .bg(cx.theme().colors().editor_background) .map(|this| { if display_count > 0 { this.child( uniform_list( "HighlightsTreeView", display_count, cx.processor(move |this, range: Range, window, cx| { this.compute_items(range, window, cx) }), ) .size_full() .track_scroll(&self.list_scroll_handle) .text_bg(cx.theme().colors().background) .into_any_element(), ) .vertical_scrollbar_for(&self.list_scroll_handle, window, cx) .into_any_element() } else { let inner_content = v_flex() .items_center() .text_center() .gap_2() .max_w_3_5() .map(|this| { if self.editor.is_some() { let has_any = !self.cached_entries.is_empty(); if has_any { this.child(Label::new("All highlights are filtered out")) .child( Label::new( "Enable text, syntax, or semantic highlights in the toolbar", ) .size(LabelSize::Small), ) } else { this.child(Label::new("No highlights found")).child( Label::new( "The editor has no text, syntax, or semantic token highlights", ) .size(LabelSize::Small), ) } } else { this.child(Label::new("Not attached to an editor")).child( Label::new("Focus an editor to show highlights") .size(LabelSize::Small), ) } }); this.h_flex() .size_full() .justify_center() .child(inner_content) .into_any_element() } }) } } impl EventEmitter<()> for HighlightsTreeView {} impl Focusable for HighlightsTreeView { fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Item for HighlightsTreeView { type Event = (); fn to_item_events(_: &Self::Event, _: &mut dyn FnMut(workspace::item::ItemEvent)) {} fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { "Highlights".into() } fn telemetry_event_text(&self) -> Option<&'static str> { None } fn can_split(&self) -> bool { true } fn clone_on_split( &self, _: Option, window: &mut Window, cx: &mut Context, ) -> Task>> where Self: Sized, { Task::ready(Some(cx.new(|cx| { let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx); clone.show_text_highlights = self.show_text_highlights; clone.show_syntax_tokens = self.show_syntax_tokens; clone.show_semantic_tokens = self.show_semantic_tokens; clone.skip_next_scroll = false; if let Some(editor) = &self.editor { clone.set_editor(editor.editor.clone(), window, cx) } clone }))) } fn on_removed(&self, cx: &mut Context) { if let Some(state) = self.editor.as_ref() { Self::clear_editor_highlights(&state.editor, cx); } } } impl Default for HighlightsTreeToolbarItemView { fn default() -> Self { Self::new() } } impl HighlightsTreeToolbarItemView { pub fn new() -> Self { Self { tree_view: None, _subscription: None, toggle_settings_handle: PopoverMenuHandle::default(), } } fn render_header(&self, cx: &Context) -> Option { let tree_view = self.tree_view.as_ref()?; let tree_view = tree_view.read(cx); let total = tree_view.cached_entries.len(); let filtered = tree_view.entry_count(); let label = if filtered == total { format!("{} highlights", total) } else { format!("{} / {} highlights", filtered, total) }; Some(ButtonLike::new("highlights header").child(Label::new(label))) } fn render_settings_button(&self, cx: &Context) -> PopoverMenu { let (show_text, show_syntax, show_semantic) = self .tree_view .as_ref() .map(|view| { let v = view.read(cx); ( v.show_text_highlights, v.show_syntax_tokens, v.show_semantic_tokens, ) }) .unwrap_or((true, true, true)); let tree_view = self.tree_view.as_ref().map(|v| v.downgrade()); PopoverMenu::new("highlights-tree-settings") .trigger_with_tooltip( IconButton::new("toggle-highlights-settings-icon", IconName::Sliders) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .toggle_state(self.toggle_settings_handle.is_deployed()), Tooltip::text("Highlights Settings"), ) .anchor(gpui::Anchor::TopRight) .with_handle(self.toggle_settings_handle.clone()) .menu(move |window, cx| { let tree_view_for_text = tree_view.clone(); let tree_view_for_syntax = tree_view.clone(); let tree_view_for_semantic = tree_view.clone(); let menu = ContextMenu::build(window, cx, move |menu, _, _| { menu.toggleable_entry( "Text Highlights", show_text, IconPosition::Start, Some(ToggleTextHighlights.boxed_clone()), { let tree_view = tree_view_for_text.clone(); move |_, cx| { if let Some(view) = tree_view.as_ref() { view.update(cx, |view, cx| { view.show_text_highlights = !view.show_text_highlights; let snapshot = view.editor.as_ref().map(|s| { s.editor.read(cx).buffer().read(cx).snapshot(cx) }); if let Some(snapshot) = snapshot { view.rebuild_display_items(&snapshot, cx); } cx.notify(); }) .ok(); } } }, ) .toggleable_entry( "Syntax Tokens", show_syntax, IconPosition::Start, Some(ToggleSyntaxTokens.boxed_clone()), { let tree_view = tree_view_for_syntax.clone(); move |_, cx| { if let Some(view) = tree_view.as_ref() { view.update(cx, |view, cx| { view.show_syntax_tokens = !view.show_syntax_tokens; let snapshot = view.editor.as_ref().map(|s| { s.editor.read(cx).buffer().read(cx).snapshot(cx) }); if let Some(snapshot) = snapshot { view.rebuild_display_items(&snapshot, cx); } cx.notify(); }) .ok(); } } }, ) .toggleable_entry( "Semantic Tokens", show_semantic, IconPosition::Start, Some(ToggleSemanticTokens.boxed_clone()), { move |_, cx| { if let Some(view) = tree_view_for_semantic.as_ref() { view.update(cx, |view, cx| { view.show_semantic_tokens = !view.show_semantic_tokens; let snapshot = view.editor.as_ref().map(|s| { s.editor.read(cx).buffer().read(cx).snapshot(cx) }); if let Some(snapshot) = snapshot { view.rebuild_display_items(&snapshot, cx); } cx.notify(); }) .ok(); } } }, ) }); Some(menu) }) } } impl Render for HighlightsTreeToolbarItemView { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { h_flex() .gap_1() .children(self.render_header(cx)) .child(self.render_settings_button(cx)) } } impl EventEmitter for HighlightsTreeToolbarItemView {} impl ToolbarItemView for HighlightsTreeToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, window: &mut Window, cx: &mut Context, ) -> ToolbarItemLocation { if let Some(item) = active_pane_item && let Some(view) = item.downcast::() { self.tree_view = Some(view.clone()); self._subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify())); return ToolbarItemLocation::PrimaryLeft; } self.tree_view = None; self._subscription = None; ToolbarItemLocation::Hidden } } fn excerpt_label_for( entry: &HighlightEntry, snapshot: &MultiBufferSnapshot, cx: &App, ) -> SharedString { let path_label = snapshot .anchor_to_buffer_anchor(entry.range.start) .and_then(|(anchor, _)| snapshot.buffer_for_id(anchor.buffer_id)) .and_then(|buf| buf.file()) .map(|file| { let full_path = file.full_path(cx); full_path.to_string_lossy().to_string() }) .unwrap_or_else(|| "untitled".to_string()); path_label.into() } fn build_highlight_entries( HighlightRefreshInput { multi_buffer_snapshot, text_highlights, semantic_highlights, syntax_theme, }: HighlightRefreshInput, ) -> Vec { let mut entries = Vec::new(); for (key, text_highlights) in text_highlights { for range in &text_highlights.1 { let Some((range_display, buffer_id, buffer_point_range)) = format_anchor_range(range, &multi_buffer_snapshot) else { continue; }; entries.push(HighlightEntry { range: range.clone(), buffer_id, range_display, style: text_highlights.0, category: HighlightCategory::Text(key), buffer_point_range, }); } } for highlight in semantic_highlights { let Some((range_display, buffer_id, buffer_point_range)) = format_anchor_range(&highlight.range, &multi_buffer_snapshot) else { continue; }; entries.push(HighlightEntry { range: highlight.range, buffer_id, range_display, style: highlight.style, category: highlight.category, buffer_point_range, }); } for excerpt_range in multi_buffer_snapshot.excerpts() { let Some(buffer_snapshot) = multi_buffer_snapshot.buffer_for_id(excerpt_range.context.start.buffer_id) else { continue; }; let start_offset = excerpt_range.context.start.to_offset(buffer_snapshot); let end_offset = excerpt_range.context.end.to_offset(buffer_snapshot); let range = start_offset..end_offset; let captures = buffer_snapshot.captures(range, |grammar| { grammar .highlights_config .as_ref() .map(|config| &config.query) }); let grammars = captures.grammars().to_vec(); let highlight_maps = grammars .iter() .map(|grammar| grammar.highlight_map()) .collect::>(); for capture in captures { let Some(highlight_id) = highlight_maps[capture.grammar_index].get(capture.index) else { continue; }; let Some(style) = syntax_theme.get(highlight_id).cloned() else { continue; }; let theme_key = syntax_theme .get_capture_name(highlight_id) .map(|theme_key| SharedString::from(theme_key.to_string())); let capture_name = grammars[capture.grammar_index] .highlights_config .as_ref() .and_then(|config| config.query.capture_names().get(capture.index as usize)) .map(|capture_name| SharedString::from((*capture_name).to_string())) .unwrap_or_else(|| SharedString::from("unknown")); let start_anchor = buffer_snapshot.anchor_before(capture.node.start_byte()); let end_anchor = buffer_snapshot.anchor_after(capture.node.end_byte()); let start = multi_buffer_snapshot.anchor_in_excerpt(start_anchor); let end = multi_buffer_snapshot.anchor_in_excerpt(end_anchor); let (start, end) = match (start, end) { (Some(start), Some(end)) => (start, end), _ => continue, }; let range = start..end; let Some((range_display, buffer_id, buffer_point_range)) = format_anchor_range(&range, &multi_buffer_snapshot) else { continue; }; entries.push(HighlightEntry { range, buffer_id, range_display, style, category: HighlightCategory::SyntaxToken { capture_name, theme_key, }, buffer_point_range, }); } } entries.sort_by(|a, b| { a.buffer_id .cmp(&b.buffer_id) .then_with(|| a.buffer_point_range.start.cmp(&b.buffer_point_range.start)) .then_with(|| a.buffer_point_range.end.cmp(&b.buffer_point_range.end)) .then_with(|| a.category.cmp(&b.category)) }); entries.dedup_by(|a, b| { a.buffer_id == b.buffer_id && a.buffer_point_range == b.buffer_point_range && a.category == b.category }); entries } fn format_anchor_range( range: &Range, snapshot: &MultiBufferSnapshot, ) -> Option<(SharedString, BufferId, Range)> { let start = range.start.to_point(snapshot); let end = range.end.to_point(snapshot); let ((start_buffer, start), (_, end)) = snapshot .point_to_buffer_point(start) .zip(snapshot.point_to_buffer_point(end))?; let display = SharedString::from(format!( "[{}:{} - {}:{}]", start.row + 1, start.column + 1, end.row + 1, end.column + 1, )); Some((display, start_buffer.remote_id(), start..end)) } fn render_style_preview(style: HighlightStyle, selected: bool, cx: &App) -> Div { let colors = cx.theme().colors(); let display_color = style.color.or(style.background_color); let mut preview = div().px_1().rounded_sm(); if let Some(color) = display_color { if selected { preview = preview.border_1().border_color(color).text_color(color); } else { preview = preview.bg(color); } } else { preview = preview.bg(colors.element_background); } let mut parts = Vec::new(); if let Some(color) = display_color { parts.push(format_hsla_as_hex(color)); } if style.font_weight.is_some() { parts.push("bold".to_string()); } if style.font_style.is_some() { parts.push("italic".to_string()); } if style.strikethrough.is_some() { parts.push("strike".to_string()); } if style.underline.is_some() { parts.push("underline".to_string()); } let label_text = if parts.is_empty() { "none".to_string() } else { parts.join(" ") }; preview.child(Label::new(label_text).size(LabelSize::Small).when_some( display_color.filter(|_| selected), |label, display_color| label.color(Color::Custom(display_color)), )) } fn format_hsla_as_hex(color: Hsla) -> String { let rgba = color.to_rgb(); let r = (rgba.r * 255.0).round() as u8; let g = (rgba.g * 255.0).round() as u8; let b = (rgba.b * 255.0).round() as u8; let a = (rgba.a * 255.0).round() as u8; if a == 255 { format!("#{:02X}{:02X}{:02X}", r, g, b) } else { format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a) } }