@@ -6,6 +6,8 @@ use tree_sitter_highlight::{
66 Highlight , HighlightConfiguration , HighlightEvent , Highlighter as TSHighlighter ,
77} ;
88
9+ use crate :: tokenize;
10+
911pub const HIGHLIGHT_NAMES : & [ & str ] = & [
1012 "attribute" ,
1113 "boolean" ,
@@ -43,6 +45,18 @@ pub struct HighlightSpan {
4345 pub highlight_id : usize ,
4446}
4547
48+ /// Resolve a capture name (e.g. `"keyword"`, `"operator"`) to its `highlight_id` — the
49+ /// index into [`HIGHLIGHT_NAMES`] that [`EditorTheme::color_for_highlight`] maps to a
50+ /// color. For the hand-written tokenizers (mermaid, latex), which emit categories by name
51+ /// rather than via a tree-sitter query. An unknown name resolves to a sentinel that renders
52+ /// as the plain foreground.
53+ pub fn highlight_id ( name : & str ) -> usize {
54+ HIGHLIGHT_NAMES
55+ . iter ( )
56+ . position ( |& n| n == name)
57+ . unwrap_or ( usize:: MAX )
58+ }
59+
4660/// Language keys are stored lowercase. Only allocate when the input actually has an
4761/// uppercase byte — the overwhelmingly common case (already-lowercase fence infos)
4862/// borrows unchanged.
@@ -54,13 +68,21 @@ fn normalized_lang(lang: &str) -> Cow<'_, str> {
5468 }
5569}
5670
57- struct LanguageConfig {
58- config : HighlightConfiguration ,
71+ /// How a registered language produces highlight spans. Most languages use a tree-sitter
72+ /// grammar + query; a few (mermaid, latex) have no publishable grammar crate and are
73+ /// lexed by a pure-Rust tokenizer instead. Both go through the same `highlight()` entry
74+ /// point, so callers (the code-block highlight cache, revealed-math styling) never
75+ /// special-case which backend a language uses.
76+ enum Backend {
77+ /// Boxed — a `HighlightConfiguration` is large, and it lives behind an `Arc` anyway.
78+ TreeSitter ( Box < HighlightConfiguration > ) ,
79+ /// `fn(source) -> spans`, each span's `highlight_id` from [`highlight_id`].
80+ Tokenizer ( fn ( & str ) -> Vec < HighlightSpan > ) ,
5981}
6082
6183pub struct Highlighter {
6284 inner : TSHighlighter ,
63- languages : HashMap < String , Arc < LanguageConfig > > ,
85+ languages : HashMap < String , Arc < Backend > > ,
6486}
6587
6688impl Default for Highlighter {
@@ -74,25 +96,40 @@ impl Highlighter {
7496 let inner = TSHighlighter :: new ( ) ;
7597 let mut languages = HashMap :: new ( ) ;
7698
77- // Register Rust
99+ let mut register = |aliases : & [ & str ] , backend : Backend | {
100+ let backend = Arc :: new ( backend) ;
101+ for a in aliases {
102+ languages. insert ( ( * a) . to_string ( ) , Arc :: clone ( & backend) ) ;
103+ }
104+ } ;
105+
106+ // Tree-sitter grammars.
78107 if let Some ( config) = Self :: create_rust_config ( ) {
79- let config = Arc :: new ( config) ;
80- languages. insert ( "rust" . to_string ( ) , Arc :: clone ( & config) ) ;
81- languages. insert ( "rs" . to_string ( ) , Arc :: clone ( & config) ) ;
108+ register ( & [ "rust" , "rs" ] , Backend :: TreeSitter ( Box :: new ( config) ) ) ;
82109 }
83-
84- // Register Bash
85110 if let Some ( config) = Self :: create_bash_config ( ) {
86- let config = Arc :: new ( config ) ;
87- languages . insert ( "bash" . to_string ( ) , Arc :: clone ( & config ) ) ;
88- languages . insert ( "sh" . to_string ( ) , Arc :: clone ( & config) ) ;
89- languages . insert ( "shell" . to_string ( ) , Arc :: clone ( & config ) ) ;
111+ register (
112+ & [ "bash" , "sh" , "shell" ] ,
113+ Backend :: TreeSitter ( Box :: new ( config) ) ,
114+ ) ;
90115 }
91116
117+ // Tokenizer-backed languages (no publishable grammar crate). Registered ungated:
118+ // a ```mermaid / ```latex code fence highlights through the normal code-block path,
119+ // and revealed `$…$` math styles via `highlight(content, "latex")`.
120+ register (
121+ & [ "mermaid" ] ,
122+ Backend :: Tokenizer ( tokenize:: highlight_mermaid) ,
123+ ) ;
124+ register (
125+ & [ "latex" , "tex" ] ,
126+ Backend :: Tokenizer ( tokenize:: highlight_latex) ,
127+ ) ;
128+
92129 Self { inner, languages }
93130 }
94131
95- fn create_rust_config ( ) -> Option < LanguageConfig > {
132+ fn create_rust_config ( ) -> Option < HighlightConfiguration > {
96133 let language = tree_sitter_rust:: LANGUAGE . into ( ) ;
97134
98135 // Use Zed's highlights.scm for better Rust coverage
@@ -116,10 +153,10 @@ impl Highlighter {
116153 // Configure which highlight names we recognize
117154 config. configure ( HIGHLIGHT_NAMES ) ;
118155
119- Some ( LanguageConfig { config } )
156+ Some ( config)
120157 }
121158
122- fn create_bash_config ( ) -> Option < LanguageConfig > {
159+ fn create_bash_config ( ) -> Option < HighlightConfiguration > {
123160 let language = tree_sitter_bash:: LANGUAGE . into ( ) ;
124161 let highlights_query = tree_sitter_bash:: HIGHLIGHT_QUERY ;
125162
@@ -134,21 +171,26 @@ impl Highlighter {
134171
135172 config. configure ( HIGHLIGHT_NAMES ) ;
136173
137- Some ( LanguageConfig { config } )
174+ Some ( config)
138175 }
139176
140177 pub fn supports_language ( & self , lang : & str ) -> bool {
141178 self . languages . contains_key ( normalized_lang ( lang) . as_ref ( ) )
142179 }
143180
144181 pub fn highlight ( & mut self , code : & str , language : & str ) -> Vec < HighlightSpan > {
145- let Some ( lang_config ) = self . languages . get ( normalized_lang ( language) . as_ref ( ) ) else {
182+ let Some ( backend ) = self . languages . get ( normalized_lang ( language) . as_ref ( ) ) else {
146183 return Vec :: new ( ) ;
147184 } ;
148185
186+ let config = match backend. as_ref ( ) {
187+ Backend :: Tokenizer ( f) => return f ( code) ,
188+ Backend :: TreeSitter ( config) => config. as_ref ( ) ,
189+ } ;
190+
149191 // Run the highlighter
150192 let highlights = match self . inner . highlight (
151- & lang_config . config ,
193+ config,
152194 code. as_bytes ( ) ,
153195 None , // cancellation flag
154196 |_| None , // injection callback (not used)
0 commit comments