From 62f759906d8278e8111061687e5f8d6bde0d6cda Mon Sep 17 00:00:00 2001 From: ozacod <47009516+ozacod@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:53:00 +0300 Subject: [PATCH] dev: Add Tree-sitter tokens and resolved theme keys in highlights tree view (#49197) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit “dev: open highlights tree view” is useful for inspecting semantic tokens. To create a theme, custom-defined semantic rules and theme settings are required and theme mappings can be difficult across languages. This PR adds syntax(tree-sitter) tokens and their resolved theme keys to tree view. It also updates semantic token entries to show their resolved theme keys. Before: before After: after Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - Added support for listing tree-sitter tokens in highlights tree view, including their resolved theme keys. Semantic token entries also show their resolved theme keys. --------- Co-authored-by: ozacod --- .../src/highlights_tree_view.rs | 181 ++++++++++++++++-- 1 file changed, 161 insertions(+), 20 deletions(-) diff --git a/crates/language_tools/src/highlights_tree_view.rs b/crates/language_tools/src/highlights_tree_view.rs index e3e6dccf9b7eedfa747e36010b9f9353b40d0275..9796c1c07375956184bdd28fbd8f5bb52bff2a32 100644 --- a/crates/language_tools/src/highlights_tree_view.rs +++ b/crates/language_tools/src/highlights_tree_view.rs @@ -8,6 +8,7 @@ use gpui::{ MouseDownEvent, MouseMoveEvent, ParentElement, Render, ScrollStrategy, SharedString, Styled, Task, UniformListScrollHandle, WeakEntity, Window, actions, div, rems, uniform_list, }; +use language::ToOffset; use menu::{SelectNext, SelectPrevious}; use std::{mem, ops::Range}; use theme::ActiveTheme; @@ -37,6 +38,8 @@ actions!( ToggleTextHighlights, /// Toggles showing semantic token highlights. ToggleSemanticTokens, + /// Toggles showing syntax token highlights. + ToggleSyntaxTokens, ] ); @@ -61,9 +64,14 @@ pub fn init(cx: &mut App) { #[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, }, } @@ -71,22 +79,34 @@ 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: Some(token_type), - token_modifiers: Some(modifiers), - } => format!("semantic token: {token_type} [{modifiers}]").into(), - HighlightCategory::SemanticToken { - token_type: Some(token_type), - token_modifiers: None, - } => format!("semantic token: {token_type}").into(), - HighlightCategory::SemanticToken { - token_type: None, - token_modifiers: Some(modifiers), - } => format!("semantic token [{modifiers}]").into(), - HighlightCategory::SemanticToken { - token_type: None, - token_modifiers: None, - } => "semantic token".into(), + 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() + } + } } } } @@ -124,6 +144,7 @@ pub struct HighlightsTreeView { display_items: Vec, is_singleton: bool, show_text_highlights: bool, + show_syntax_tokens: bool, show_semantic_tokens: bool, skip_next_scroll: bool, } @@ -157,6 +178,7 @@ impl HighlightsTreeView { display_items: Vec::new(), is_singleton: true, show_text_highlights: true, + show_syntax_tokens: true, show_semantic_tokens: true, skip_next_scroll: false, }; @@ -280,6 +302,7 @@ impl HighlightsTreeView { let mut entries = Vec::new(); + let semantic_theme = cx.theme().syntax().clone(); display_map.update(cx, |display_map, cx| { for (key, text_highlights) in display_map.all_text_highlights() { for range in &text_highlights.1 { @@ -323,6 +346,32 @@ impl HighlightsTreeView { ) 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| { + semantic_theme.get_opt(style_name).is_some() + }) + .map(|style_name| { + SharedString::from(style_name.clone()) + }) + .or(theme_key) + }) + }); + entries.push(HighlightEntry { excerpt_id, range, @@ -333,6 +382,7 @@ impl HighlightsTreeView { token_modifiers: stylizer .token_modifiers(token.token_modifiers) .map(SharedString::from), + theme_key, }, sort_key, }); @@ -341,6 +391,66 @@ impl HighlightsTreeView { }); }); + let syntax_theme = cx.theme().syntax().clone(); + for (excerpt_id, buffer_snapshot, excerpt_range) in multi_buffer_snapshot.excerpts() { + 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 + .syntax + .captures(range, buffer_snapshot, |grammar| { + grammar.highlights_config.as_ref().map(|c| &c.query) + }); + let grammars: Vec<_> = captures.grammars().to_vec(); + let highlight_maps: Vec<_> = grammars.iter().map(|g| g.highlight_map()).collect(); + + for capture in captures { + let highlight_id = highlight_maps[capture.grammar_index].get(capture.index); + let Some(style) = highlight_id.style(&syntax_theme) else { + continue; + }; + + let theme_key = highlight_id + .name(&syntax_theme) + .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(excerpt_id, start_anchor); + let end = multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, end_anchor); + + let (start, end) = match (start, end) { + (Some(s), Some(e)) => (s, e), + _ => continue, + }; + + let range = start..end; + let (range_display, sort_key) = + format_anchor_range(&range, excerpt_id, &multi_buffer_snapshot, is_singleton); + + entries.push(HighlightEntry { + excerpt_id, + range, + range_display, + style, + category: HighlightCategory::SyntaxToken { + capture_name, + theme_key, + }, + sort_key, + }); + } + } + entries.sort_by(|a, b| { a.sort_key .cmp(&b.sort_key) @@ -387,6 +497,7 @@ impl HighlightsTreeView { 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, } } @@ -695,14 +806,14 @@ impl Render for HighlightsTreeView { this.child(Label::new("All highlights are filtered out")) .child( Label::new( - "Enable text or semantic highlights in the toolbar", + "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 or semantic token highlights", + "The editor has no text, syntax, or semantic token highlights", ) .size(LabelSize::Small), ) @@ -762,6 +873,7 @@ impl Item for HighlightsTreeView { 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 { @@ -810,14 +922,18 @@ impl HighlightsTreeToolbarItemView { } fn render_settings_button(&self, cx: &Context) -> PopoverMenu { - let (show_text, show_semantic) = self + 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_semantic_tokens) + ( + v.show_text_highlights, + v.show_syntax_tokens, + v.show_semantic_tokens, + ) }) - .unwrap_or((true, true)); + .unwrap_or((true, true, true)); let tree_view = self.tree_view.as_ref().map(|v| v.downgrade()); @@ -833,6 +949,7 @@ impl HighlightsTreeToolbarItemView { .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, _, _| { @@ -860,6 +977,30 @@ impl HighlightsTreeToolbarItemView { } }, ) + .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,