1- use std:: { iter, ops:: Range } ;
1+ use std:: { cell :: RefCell , collections :: HashMap , iter, ops:: Range , sync :: Arc } ;
22
3- use arborium:: tree_sitter:: { Node , Query , QueryCursor , TextProvider , Tree } ;
3+ use arborium:: tree_sitter:: { Node , Parser , Query , QueryCursor , TextProvider , Tree } ;
44use rangemap:: RangeMap ;
55use streaming_iterator:: StreamingIterator ;
66use string_offset:: { ByteOffset , CharOffset } ;
@@ -10,6 +10,10 @@ use warp_editor::content::{
1010} ;
1111use warpui:: color:: ColorU ;
1212
13+ thread_local ! {
14+ static INJECTION_PARSER : RefCell <Parser > = RefCell :: new( Parser :: new( ) ) ;
15+ }
16+
1317/// Color mapping from parsed syntax token name to its corresponding highlighting color.
1418#[ derive( Clone , Copy ) ]
1519pub struct ColorMap {
@@ -28,6 +32,11 @@ pub struct HighlightQuery {
2832 highlight_map : Vec < Option < ColorU > > ,
2933}
3034
35+ pub struct InjectionHighlightQuery {
36+ pub language : Arc < languages:: Language > ,
37+ pub highlight_query : HighlightQuery ,
38+ }
39+
3140impl HighlightQuery {
3241 pub fn new ( query : & Query , color_map : ColorMap ) -> Self {
3342 let highlight_map = query
@@ -77,6 +86,180 @@ impl HighlightQuery {
7786
7887 range_map
7988 }
89+
90+ /// Process language injections and return highlights for injected regions.
91+ /// This handles embedded languages like JavaScript/CSS in Vue files.
92+ pub fn get_injection_highlights (
93+ & self ,
94+ range : Range < CharOffset > ,
95+ injections_query : & Query ,
96+ injection_highlight_queries : & HashMap < String , InjectionHighlightQuery > ,
97+ buffer : & Buffer ,
98+ tree : & Tree ,
99+ ) -> RangeMap < CharOffset , ColorU > {
100+ let mut range_map = RangeMap :: new ( ) ;
101+
102+ let mut cursor = QueryCursor :: new ( ) ;
103+ let byte_start = range. start . to_buffer_byte_offset ( buffer) . as_usize ( ) ;
104+ let byte_end = range. end . to_buffer_byte_offset ( buffer) . as_usize ( ) ;
105+ cursor. set_byte_range ( byte_start..byte_end) ;
106+
107+ let injection_content_idx = injections_query
108+ . capture_names ( )
109+ . iter ( )
110+ . position ( |name| * name == "injection.content" )
111+ . map ( |idx| idx as u32 ) ;
112+ let injection_language_idx = injections_query
113+ . capture_names ( )
114+ . iter ( )
115+ . position ( |name| * name == "injection.language" )
116+ . map ( |idx| idx as u32 ) ;
117+
118+ let mut matches = cursor. matches ( injections_query, tree. root_node ( ) , TextBuffer ( buffer) ) ;
119+
120+ while let Some ( query_match) = matches. next ( ) {
121+ // 中文注释:优先读取 query 的 #set! 属性,这是注入语言的权威来源。
122+ // Source: arborium-highlight-2.18.0/src/tree_sitter.rs
123+ let mut lang_name = injections_query
124+ . property_settings ( query_match. pattern_index )
125+ . iter ( )
126+ . find ( |prop| prop. key . as_ref ( ) == "injection.language" )
127+ . and_then ( |prop| prop. value . as_deref ( ) )
128+ . map ( str:: to_owned) ;
129+ let mut content_node: Option < Node > = None ;
130+
131+ for cap in query_match. captures {
132+ if Some ( cap. index ) == injection_content_idx {
133+ content_node = Some ( cap. node ) ;
134+ } else if Some ( cap. index ) == injection_language_idx && lang_name. is_none ( ) {
135+ let bytes = collect_buffer_bytes (
136+ buffer,
137+ ByteOffset :: from ( cap. node . start_byte ( ) ) ,
138+ ByteOffset :: from ( cap. node . end_byte ( ) ) ,
139+ ) ;
140+ if let Ok ( value) = std:: str:: from_utf8 ( & bytes) {
141+ if !value. is_empty ( ) {
142+ lang_name = Some ( value. to_string ( ) ) ;
143+ }
144+ }
145+ }
146+ }
147+
148+ if let ( Some ( node) , Some ( lang_name) ) = ( content_node, lang_name) {
149+ let normalized_lang = normalize_injection_language_name ( & lang_name) ;
150+
151+ if let Some ( injection_query) = injection_highlight_queries. get ( normalized_lang) {
152+ let content_range = node. byte_range ( ) ;
153+ let local_start = byte_start. saturating_sub ( content_range. start ) ;
154+ let local_end = byte_end
155+ . min ( content_range. end )
156+ . saturating_sub ( content_range. start ) ;
157+
158+ if local_start < local_end {
159+ let source = collect_buffer_bytes (
160+ buffer,
161+ ByteOffset :: from ( content_range. start ) ,
162+ ByteOffset :: from ( content_range. end ) ,
163+ ) ;
164+
165+ // 中文注释:注入片段必须先按目标语言单独建树,再把高亮偏移回原文件。
166+ let highlights = injection_query
167+ . highlight_query
168+ . get_highlighted_chunks_for_injection (
169+ local_start..local_end,
170+ & injection_query. language ,
171+ & source,
172+ content_range. start ,
173+ buffer,
174+ ) ;
175+
176+ for ( highlight_range, color) in highlights. iter ( ) {
177+ range_map. insert ( highlight_range. clone ( ) , * color) ;
178+ }
179+ }
180+ }
181+ }
182+ }
183+
184+ range_map
185+ }
186+
187+ fn get_highlighted_chunks_for_injection (
188+ & self ,
189+ local_byte_range : Range < usize > ,
190+ language : & languages:: Language ,
191+ source : & [ u8 ] ,
192+ base_byte_offset : usize ,
193+ buffer : & Buffer ,
194+ ) -> RangeMap < CharOffset , ColorU > {
195+ INJECTION_PARSER . with ( |parser| {
196+ let mut parser = parser. borrow_mut ( ) ;
197+ parser
198+ . set_language ( & language. grammar )
199+ . expect ( "注入语言语法应当兼容 parser" ) ;
200+
201+ let Some ( tree) = parser. parse ( source, None ) else {
202+ return RangeMap :: new ( ) ;
203+ } ;
204+
205+ let mut range_map = RangeMap :: new ( ) ;
206+ let mut cursor = QueryCursor :: new ( ) ;
207+ cursor. set_byte_range ( local_byte_range) ;
208+ let mut captures = cursor. captures (
209+ & language. highlight_query ,
210+ tree. root_node ( ) ,
211+ TextSlice ( source) ,
212+ ) ;
213+
214+ while let Some ( matches) = captures. next ( ) {
215+ for cap in matches. 0 . captures {
216+ let insertion_range = cap. node . byte_range ( ) ;
217+ let color = self
218+ . highlight_map
219+ . get ( cap. index as usize )
220+ . and_then ( |inner| * inner) ;
221+
222+ if let Some ( color) = color {
223+ let global_start =
224+ injection_byte_to_buffer_byte ( base_byte_offset, insertion_range. start ) ;
225+ let global_end =
226+ injection_byte_to_buffer_byte ( base_byte_offset, insertion_range. end ) ;
227+ let char_start =
228+ ByteOffset :: from ( global_start) . to_buffer_char_offset ( buffer) ;
229+ let char_end = ByteOffset :: from ( global_end) . to_buffer_char_offset ( buffer) ;
230+ if char_start < char_end {
231+ range_map. insert ( char_start..char_end, color) ;
232+ }
233+ }
234+ }
235+ }
236+
237+ range_map
238+ } )
239+ }
240+ }
241+
242+ fn injection_byte_to_buffer_byte ( base_byte_offset : usize , local_byte_offset : usize ) -> usize {
243+ // 中文注释:注入片段来自已解析的 Vue tree-sitter 节点,映射回 Buffer 时需要抵消
244+ // Buffer/TreeSitter 边界上的 1-byte 偏移,避免 token 首字符漏高亮。
245+ base_byte_offset + local_byte_offset. saturating_sub ( 1 )
246+ }
247+
248+ fn normalize_injection_language_name ( name : & str ) -> & str {
249+ match name {
250+ "js" => "javascript" ,
251+ "ts" => "typescript" ,
252+ "less" | "postcss" => "scss" ,
253+ other => other,
254+ }
255+ }
256+
257+ fn collect_buffer_bytes ( buffer : & Buffer , start : ByteOffset , end : ByteOffset ) -> Vec < u8 > {
258+ let mut bytes = Vec :: new ( ) ;
259+ for chunk in buffer. bytes_in_range ( start, end) {
260+ bytes. extend_from_slice ( chunk) ;
261+ }
262+ bytes
80263}
81264
82265fn convert_capture_name_to_color ( name : & str , color_map : & ColorMap ) -> Option < ColorU > {
0 commit comments