1use std::collections::HashMap;
6use std::ops::Range;
7use std::path::Path;
8
9use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator, Tree};
10use tree_sitter_highlight::HighlightConfiguration;
11
12use crate::syntax::MAX_PARSE_BYTES;
13
14pub const HIGHLIGHT_NAMES: &[&str] = &[
18 "attribute",
19 "comment",
20 "constant",
21 "constant.builtin",
22 "constructor",
23 "function",
24 "function.builtin",
25 "keyword",
26 "label",
27 "number",
28 "operator",
29 "property",
30 "punctuation",
31 "punctuation.bracket",
32 "punctuation.delimiter",
33 "string",
34 "string.escape",
35 "string.special",
36 "tag",
37 "text.emphasis",
38 "text.literal",
39 "text.reference",
40 "text.strong",
41 "text.title",
42 "text.uri",
43 "type",
44 "type.builtin",
45 "variable",
46 "variable.builtin",
47 "variable.parameter",
48];
49
50pub struct LangEntry {
51 pub name: &'static str,
52 pub language: Language,
53 pub config: Option<HighlightConfiguration>,
56 pub tags: Option<Query>,
59}
60
61pub struct LanguageRegistry {
62 entries: Vec<LangEntry>,
63 by_ext: HashMap<&'static str, usize>,
64 by_name: HashMap<&'static str, usize>,
65 markdown_inline_query: Option<Query>,
69}
70
71impl LanguageRegistry {
72 #[allow(clippy::too_many_lines)]
75 pub fn build() -> Self {
76 let mut r = Self {
77 entries: Vec::new(),
78 by_ext: HashMap::new(),
79 by_name: HashMap::new(),
80 markdown_inline_query: None,
81 };
82
83 r.add(
84 "rust",
85 &["rs"],
86 tree_sitter_rust::LANGUAGE.into(),
87 tree_sitter_rust::HIGHLIGHTS_QUERY,
88 Some(tree_sitter_rust::TAGS_QUERY),
89 );
90 r.add(
91 "python",
92 &["py", "pyi"],
93 tree_sitter_python::LANGUAGE.into(),
94 tree_sitter_python::HIGHLIGHTS_QUERY,
95 Some(tree_sitter_python::TAGS_QUERY),
96 );
97 r.add(
98 "javascript",
99 &["js", "jsx", "mjs", "cjs"],
100 tree_sitter_javascript::LANGUAGE.into(),
101 &format!(
102 "{}\n{}",
103 tree_sitter_javascript::HIGHLIGHT_QUERY,
104 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY
105 ),
106 Some(tree_sitter_javascript::TAGS_QUERY),
107 );
108 r.add(
109 "typescript",
110 &["ts", "mts", "cts"],
111 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
112 &format!(
113 "{}\n{}",
114 tree_sitter_javascript::HIGHLIGHT_QUERY,
115 tree_sitter_typescript::HIGHLIGHTS_QUERY
116 ),
117 Some(tree_sitter_typescript::TAGS_QUERY),
118 );
119 r.add(
120 "tsx",
121 &["tsx"],
122 tree_sitter_typescript::LANGUAGE_TSX.into(),
123 &format!(
124 "{}\n{}\n{}",
125 tree_sitter_javascript::HIGHLIGHT_QUERY,
126 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
127 tree_sitter_typescript::HIGHLIGHTS_QUERY
128 ),
129 Some(tree_sitter_typescript::TAGS_QUERY),
130 );
131 r.add(
132 "go",
133 &["go"],
134 tree_sitter_go::LANGUAGE.into(),
135 tree_sitter_go::HIGHLIGHTS_QUERY,
136 Some(tree_sitter_go::TAGS_QUERY),
137 );
138 r.add(
139 "c",
140 &["c", "h"],
141 tree_sitter_c::LANGUAGE.into(),
142 tree_sitter_c::HIGHLIGHT_QUERY,
143 Some(tree_sitter_c::TAGS_QUERY),
144 );
145 r.add(
146 "cpp",
147 &["cpp", "cc", "cxx", "hpp", "hh", "hxx"],
148 tree_sitter_cpp::LANGUAGE.into(),
149 tree_sitter_cpp::HIGHLIGHT_QUERY,
150 Some(tree_sitter_cpp::TAGS_QUERY),
151 );
152 r.add(
153 "java",
154 &["java"],
155 tree_sitter_java::LANGUAGE.into(),
156 tree_sitter_java::HIGHLIGHTS_QUERY,
157 Some(tree_sitter_java::TAGS_QUERY),
158 );
159 r.add(
160 "c-sharp",
161 &["cs"],
162 tree_sitter_c_sharp::LANGUAGE.into(),
163 tree_sitter_c_sharp::HIGHLIGHTS_QUERY,
164 Some(tree_sitter_c_sharp::TAGS_QUERY),
165 );
166 r.add(
167 "ruby",
168 &["rb"],
169 tree_sitter_ruby::LANGUAGE.into(),
170 tree_sitter_ruby::HIGHLIGHTS_QUERY,
171 Some(tree_sitter_ruby::TAGS_QUERY),
172 );
173 r.add(
174 "php",
175 &["php"],
176 tree_sitter_php::LANGUAGE_PHP.into(),
177 tree_sitter_php::HIGHLIGHTS_QUERY,
178 Some(tree_sitter_php::TAGS_QUERY),
179 );
180 r.add(
181 "bash",
182 &["sh", "bash", "zsh"],
183 tree_sitter_bash::LANGUAGE.into(),
184 tree_sitter_bash::HIGHLIGHT_QUERY,
185 None,
186 );
187 r.add(
188 "json",
189 &["json"],
190 tree_sitter_json::LANGUAGE.into(),
191 tree_sitter_json::HIGHLIGHTS_QUERY,
192 None,
193 );
194 r.add(
195 "html",
196 &["html", "htm"],
197 tree_sitter_html::LANGUAGE.into(),
198 tree_sitter_html::HIGHLIGHTS_QUERY,
199 None,
200 );
201 r.add(
202 "css",
203 &["css"],
204 tree_sitter_css::LANGUAGE.into(),
205 tree_sitter_css::HIGHLIGHTS_QUERY,
206 None,
207 );
208 r.add(
209 "yaml",
210 &["yml", "yaml"],
211 tree_sitter_yaml::LANGUAGE.into(),
212 tree_sitter_yaml::HIGHLIGHTS_QUERY,
213 None,
214 );
215 r.register(
219 "markdown",
220 &["md", "markdown"],
221 tree_sitter_md::LANGUAGE.into(),
222 tree_sitter_md::HIGHLIGHT_QUERY_BLOCK,
223 tree_sitter_md::INJECTION_QUERY_BLOCK,
224 None,
225 );
226 let md_inline: Language = tree_sitter_md::INLINE_LANGUAGE.into();
227 r.markdown_inline_query =
228 Query::new(&md_inline, tree_sitter_md::HIGHLIGHT_QUERY_INLINE).ok();
229 r.register(
230 "markdown_inline",
231 &[],
232 md_inline,
233 tree_sitter_md::HIGHLIGHT_QUERY_INLINE,
234 "",
235 None,
236 );
237
238 r
239 }
240
241 fn add(
242 &mut self,
243 name: &'static str,
244 extensions: &'static [&'static str],
245 language: Language,
246 highlights: &str,
247 tags: Option<&str>,
248 ) {
249 self.register(name, extensions, language, highlights, "", tags);
250 }
251
252 fn register(
253 &mut self,
254 name: &'static str,
255 extensions: &'static [&'static str],
256 language: Language,
257 highlights: &str,
258 injections: &str,
259 tags: Option<&str>,
260 ) {
261 let config =
262 HighlightConfiguration::new(language.clone(), name, highlights, injections, "")
263 .ok()
264 .map(|mut c| {
265 c.configure(HIGHLIGHT_NAMES);
266 c
267 });
268 let tags = tags.and_then(|q| Query::new(&language, q).ok());
269 let idx = self.entries.len();
270 self.entries.push(LangEntry {
271 name,
272 language,
273 config,
274 tags,
275 });
276 self.by_name.insert(name, idx);
277 for ext in extensions {
278 self.by_ext.insert(ext, idx);
279 }
280 }
281
282 pub fn for_path(&self, path: &str) -> Option<&LangEntry> {
284 let ext = Path::new(path).extension()?.to_str()?;
285 let &idx = self.by_ext.get(ext)?;
286 self.entries.get(idx)
287 }
288
289 pub fn for_token(&self, token: &str) -> Option<&LangEntry> {
292 let token = token.trim().to_ascii_lowercase();
293 let token = match token.as_str() {
294 "c++" => "cpp",
295 "c#" | "csharp" => "cs",
296 "shell" => "bash",
297 "golang" => "go",
298 other => other,
299 };
300 let &idx = self.by_name.get(token).or_else(|| self.by_ext.get(token))?;
301 self.entries.get(idx)
302 }
303
304 pub fn config_for_injection(&self, lang: &str) -> Option<&HighlightConfiguration> {
308 self.for_token(lang)?.config.as_ref()
309 }
310
311 pub fn markdown_inline_spans(&self, content: &str) -> Vec<(Range<usize>, &'static str)> {
316 if content.len() > MAX_PARSE_BYTES {
317 return Vec::new();
318 }
319 let (Some(query), Some(block), Some(inline)) = (
320 self.markdown_inline_query.as_ref(),
321 self.by_name
322 .get("markdown")
323 .and_then(|&i| self.entries.get(i)),
324 self.by_name
325 .get("markdown_inline")
326 .and_then(|&i| self.entries.get(i)),
327 ) else {
328 return Vec::new();
329 };
330
331 let mut bp = Parser::new();
332 if bp.set_language(&block.language).is_err() {
333 return Vec::new();
334 }
335 let Some(block_tree) = bp.parse(content, None) else {
336 return Vec::new();
337 };
338 let ranges = inline_node_ranges(&block_tree);
339 if ranges.is_empty() {
340 return Vec::new();
341 }
342
343 let mut ip = Parser::new();
344 if ip.set_included_ranges(&ranges).is_err() || ip.set_language(&inline.language).is_err() {
345 return Vec::new();
346 }
347 let Some(inline_tree) = ip.parse(content, None) else {
348 return Vec::new();
349 };
350
351 let names = query.capture_names();
352 let mut cursor = QueryCursor::new();
353 let mut spans = Vec::new();
354 let mut matches = cursor.matches(query, inline_tree.root_node(), content.as_bytes());
355 while let Some(m) = matches.next() {
356 for cap in m.captures {
357 let cname = names.get(cap.index as usize).copied().unwrap_or("");
358 if let Some(name) = recognized_highlight(cname) {
359 spans.push((cap.node.byte_range(), name));
360 }
361 }
362 }
363 spans.sort_by_key(|(r, _)| r.end - r.start);
364 spans
365 }
366}
367
368fn inline_node_ranges(tree: &Tree) -> Vec<tree_sitter::Range> {
371 let mut ranges = Vec::new();
372 let mut cursor = tree.walk();
373 loop {
374 let node = cursor.node();
375 if node.kind() == "inline" && node.end_byte() > node.start_byte() {
376 ranges.push(node.range());
377 }
378 if cursor.goto_first_child() {
379 continue;
380 }
381 loop {
382 if cursor.goto_next_sibling() {
383 break;
384 }
385 if !cursor.goto_parent() {
386 return ranges;
387 }
388 }
389 }
390}
391
392fn recognized_highlight(capture: &str) -> Option<&'static str> {
395 HIGHLIGHT_NAMES
396 .iter()
397 .copied()
398 .filter(|name| {
399 capture == *name
400 || capture
401 .strip_prefix(name)
402 .is_some_and(|r| r.starts_with('.'))
403 })
404 .max_by_key(|name| name.len())
405}
406
407impl Default for LanguageRegistry {
408 fn default() -> Self {
409 Self::build()
410 }
411}