Skip to content

Commit 2fbfaff

Browse files
authored
feat: 添加 Vue 文件 injection 高亮支持(JS/CSS/TS) (#218)
- 在 Language 结构体中添加 injections_query 字段 - 加载 arborium 的 Vue INJECTIONS_QUERY - 实现 injection 高亮处理逻辑 - 支持 <script>、<script lang="ts">、<style> 等标签内的代码高亮
1 parent bbcdbc1 commit 2fbfaff

3 files changed

Lines changed: 244 additions & 3 deletions

File tree

crates/languages/src/lib.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,8 @@ pub struct Language {
228228
pub grammar: ParserGrammar,
229229
/// Query for syntax highlighting.
230230
pub highlight_query: Query,
231+
/// Query for language injections (e.g., JS/CSS in Vue files).
232+
pub injections_query: Option<Query>,
231233
/// Query for auto indent.
232234
pub indents_query: Option<Query>,
233235
/// Unit for each indent action.
@@ -331,6 +333,16 @@ fn get_arborium_highlight_query(lang: &str) -> Option<&str> {
331333
}
332334
}
333335

336+
/// Get the bundled injections query from arborium for a given language.
337+
/// This enables embedded language highlighting (e.g., JS/CSS in Vue files).
338+
fn get_arborium_injections_query(lang: &str) -> Option<&str> {
339+
match lang {
340+
"vue" => Some(arborium::lang_vue::INJECTIONS_QUERY),
341+
// Add other languages with injections support here as needed
342+
_ => None,
343+
}
344+
}
345+
334346
fn load_language(lang: &str) -> Option<Language> {
335347
let arborium_name = to_arborium_name(lang);
336348
let grammar = arborium::get_language(arborium_name)?;
@@ -354,6 +366,10 @@ fn load_language(lang: &str) -> Option<Language> {
354366
let highlight_query = Query::new(&grammar, highlight_query_str)
355367
.expect("arborium highlight query should be valid");
356368

369+
// Load injections query for embedded language support (e.g., JS/CSS in Vue)
370+
let injections_query = get_arborium_injections_query(lang)
371+
.and_then(|query_str| Query::new(&grammar, query_str).ok());
372+
357373
let indents_query_path = [lang, "indents.scm"].join("\\");
358374
let indents_query = load_query(&indents_query_path, &grammar);
359375

@@ -362,6 +378,7 @@ fn load_language(lang: &str) -> Option<Language> {
362378

363379
Some(Language {
364380
highlight_query,
381+
injections_query,
365382
indents_query,
366383
grammar,
367384
indent_unit,

crates/syntax_tree/src/lib.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use parking_lot::Mutex;
1313
use arborium::tree_sitter::{InputEdit, Parser, Tree};
1414
use futures::stream::AbortHandle;
1515
use queries::{
16-
highlight_query::HighlightQuery,
16+
highlight_query::{HighlightQuery, InjectionHighlightQuery},
1717
indent_query::{indentation_delta, IndentDelta},
1818
};
1919
use rangemap::{RangeMap, RangeSet};
@@ -43,6 +43,8 @@ pub enum DecorationStateEvent {
4343
struct LanguageQueries {
4444
language: Arc<Language>,
4545
syntax_query: HighlightQuery,
46+
/// Highlight queries for injected languages (e.g., JavaScript, CSS for Vue)
47+
injection_queries: HashMap<String, InjectionHighlightQuery>,
4648
}
4749

4850
/// Single-entry cache for highlight queries.
@@ -109,9 +111,29 @@ impl SyntaxTreeState {
109111
}
110112

111113
pub fn set_language(&mut self, language: Arc<Language>) {
114+
let mut injection_queries = HashMap::new();
115+
116+
if language.injections_query.is_some() {
117+
// 中文注释:这里按 Vue injections.scm 里会出现的语言集合预热查询,避免渲染时重复查表。
118+
for injection_language in ["javascript", "typescript", "jsx", "tsx", "css", "scss"] {
119+
if let Some(injected_language) = languages::language_by_name(injection_language) {
120+
let highlight_query =
121+
HighlightQuery::new(&injected_language.highlight_query, self.color_map);
122+
injection_queries.insert(
123+
injection_language.to_string(),
124+
InjectionHighlightQuery {
125+
language: injected_language,
126+
highlight_query,
127+
},
128+
);
129+
}
130+
}
131+
}
132+
112133
self.language_queries = Some(LanguageQueries {
113134
syntax_query: HighlightQuery::new(&language.highlight_query, self.color_map),
114135
language,
136+
injection_queries,
115137
});
116138
}
117139

@@ -183,6 +205,25 @@ impl SyntaxTreeState {
183205
for (highlight_range, color) in highlights.iter() {
184206
combined_highlights.insert(highlight_range.clone(), *color);
185207
}
208+
209+
// Process injections for embedded languages (e.g., JS/CSS in Vue)
210+
if let Some(injections_query) = &language_queries.language.injections_query {
211+
if !language_queries.injection_queries.is_empty() {
212+
let injection_highlights =
213+
language_queries.syntax_query.get_injection_highlights(
214+
range.clone(),
215+
injections_query,
216+
&language_queries.injection_queries,
217+
buffer.as_ref(ctx),
218+
tree,
219+
);
220+
221+
// Merge injection highlights (these override main highlights)
222+
for (highlight_range, color) in injection_highlights.iter() {
223+
combined_highlights.insert(highlight_range.clone(), *color);
224+
}
225+
}
226+
}
186227
}
187228

188229
// Once we have rendered content version X, we could discard syntax trees belonging to versions before X.

crates/syntax_tree/src/queries/highlight_query.rs

Lines changed: 185 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
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};
44
use rangemap::RangeMap;
55
use streaming_iterator::StreamingIterator;
66
use string_offset::{ByteOffset, CharOffset};
@@ -10,6 +10,10 @@ use warp_editor::content::{
1010
};
1111
use 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)]
1519
pub 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+
3140
impl 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

82265
fn convert_capture_name_to_color(name: &str, color_map: &ColorMap) -> Option<ColorU> {

0 commit comments

Comments
 (0)