1use std::borrow::Cow;
6use std::collections::HashMap;
7use std::ops::Range;
8use std::path::Path;
9use std::sync::{LazyLock, OnceLock};
10
11use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator, Tree};
12use tree_sitter_highlight::HighlightConfiguration;
13
14use crate::syntax::MAX_PARSE_BYTES;
15
16pub const HIGHLIGHT_NAMES: &[&str] = &[
20 "attribute",
21 "boolean",
22 "comment",
23 "conditional",
24 "constant",
25 "constant.builtin",
26 "constructor",
27 "field",
28 "function",
29 "function.builtin",
30 "keyword",
31 "label",
32 "number",
33 "operator",
34 "parameter",
35 "property",
36 "punctuation",
37 "punctuation.bracket",
38 "punctuation.delimiter",
39 "spell",
40 "storageclass",
41 "string",
42 "string.escape",
43 "string.special",
44 "tag",
45 "text.emphasis",
46 "text.literal",
47 "text.reference",
48 "text.strong",
49 "text.title",
50 "text.uri",
51 "type",
52 "type.builtin",
53 "variable",
54 "variable.builtin",
55 "variable.parameter",
56];
57
58pub struct LangEntry {
59 pub name: &'static str,
60 pub language: Language,
61 highlights: Cow<'static, str>,
62 injections: Cow<'static, str>,
63 tags_query: Option<Cow<'static, str>>,
64 config: OnceLock<Option<HighlightConfiguration>>,
68 tags: OnceLock<Option<Query>>,
69}
70
71impl LangEntry {
72 pub fn config(&self) -> Option<&HighlightConfiguration> {
73 self.config
74 .get_or_init(|| {
75 HighlightConfiguration::new(
76 self.language.clone(),
77 self.name,
78 &self.highlights,
79 &self.injections,
80 "",
81 )
82 .ok()
83 .map(|mut config| {
84 config.configure(HIGHLIGHT_NAMES);
85 config
86 })
87 })
88 .as_ref()
89 }
90
91 pub fn tags(&self) -> Option<&Query> {
92 self.tags
93 .get_or_init(|| Query::new(&self.language, self.tags_query.as_deref()?).ok())
94 .as_ref()
95 }
96}
97
98pub static REGISTRY: LazyLock<LanguageRegistry> = LazyLock::new(LanguageRegistry::build);
102
103pub struct LanguageRegistry {
104 entries: Vec<LangEntry>,
105 by_ext: HashMap<&'static str, usize>,
106 by_name: HashMap<&'static str, usize>,
107 by_filename: HashMap<&'static str, usize>,
108 markdown_inline_query: Option<Query>,
112}
113
114impl LanguageRegistry {
115 #[allow(clippy::too_many_lines)]
118 pub fn build() -> Self {
119 let mut r = Self {
120 entries: Vec::new(),
121 by_ext: HashMap::new(),
122 by_name: HashMap::new(),
123 by_filename: HashMap::new(),
124 markdown_inline_query: None,
125 };
126
127 r.add(
128 "rust",
129 &["rs"],
130 tree_sitter_rust::LANGUAGE.into(),
131 tree_sitter_rust::HIGHLIGHTS_QUERY,
132 Some(tree_sitter_rust::TAGS_QUERY),
133 );
134 r.add(
135 "python",
136 &["py", "pyi"],
137 tree_sitter_python::LANGUAGE.into(),
138 tree_sitter_python::HIGHLIGHTS_QUERY,
139 Some(tree_sitter_python::TAGS_QUERY),
140 );
141 r.add(
142 "javascript",
143 &["js", "jsx", "mjs", "cjs"],
144 tree_sitter_javascript::LANGUAGE.into(),
145 format!(
146 "{}\n{}",
147 tree_sitter_javascript::HIGHLIGHT_QUERY,
148 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY
149 ),
150 Some(tree_sitter_javascript::TAGS_QUERY),
151 );
152 r.add(
153 "typescript",
154 &["ts", "mts", "cts"],
155 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
156 format!(
157 "{}\n{}",
158 tree_sitter_javascript::HIGHLIGHT_QUERY,
159 tree_sitter_typescript::HIGHLIGHTS_QUERY
160 ),
161 Some(tree_sitter_typescript::TAGS_QUERY),
162 );
163 r.add(
164 "tsx",
165 &["tsx"],
166 tree_sitter_typescript::LANGUAGE_TSX.into(),
167 format!(
168 "{}\n{}\n{}",
169 tree_sitter_javascript::HIGHLIGHT_QUERY,
170 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
171 tree_sitter_typescript::HIGHLIGHTS_QUERY
172 ),
173 Some(tree_sitter_typescript::TAGS_QUERY),
174 );
175 r.add(
176 "go",
177 &["go"],
178 tree_sitter_go::LANGUAGE.into(),
179 tree_sitter_go::HIGHLIGHTS_QUERY,
180 Some(tree_sitter_go::TAGS_QUERY),
181 );
182 r.add(
183 "c",
184 &["c", "h"],
185 tree_sitter_c::LANGUAGE.into(),
186 tree_sitter_c::HIGHLIGHT_QUERY,
187 Some(tree_sitter_c::TAGS_QUERY),
188 );
189 r.add(
190 "cpp",
191 &["cpp", "cc", "cxx", "hpp", "hh", "hxx"],
192 tree_sitter_cpp::LANGUAGE.into(),
193 format!(
195 "{}\n{}",
196 tree_sitter_c::HIGHLIGHT_QUERY,
197 tree_sitter_cpp::HIGHLIGHT_QUERY
198 ),
199 Some(tree_sitter_cpp::TAGS_QUERY),
200 );
201 r.add(
202 "java",
203 &["java"],
204 tree_sitter_java::LANGUAGE.into(),
205 tree_sitter_java::HIGHLIGHTS_QUERY,
206 Some(tree_sitter_java::TAGS_QUERY),
207 );
208 r.add(
209 "c-sharp",
210 &["cs"],
211 tree_sitter_c_sharp::LANGUAGE.into(),
212 tree_sitter_c_sharp::HIGHLIGHTS_QUERY,
213 Some(tree_sitter_c_sharp::TAGS_QUERY),
214 );
215 r.add(
216 "ruby",
217 &["rb"],
218 tree_sitter_ruby::LANGUAGE.into(),
219 tree_sitter_ruby::HIGHLIGHTS_QUERY,
220 Some(tree_sitter_ruby::TAGS_QUERY),
221 );
222 r.add(
223 "php",
224 &["php"],
225 tree_sitter_php::LANGUAGE_PHP.into(),
226 tree_sitter_php::HIGHLIGHTS_QUERY,
227 Some(tree_sitter_php::TAGS_QUERY),
228 );
229 r.add(
230 "bash",
231 &["sh", "bash", "zsh"],
232 tree_sitter_bash::LANGUAGE.into(),
233 tree_sitter_bash::HIGHLIGHT_QUERY,
234 None,
235 );
236 r.add(
237 "json",
238 &["json"],
239 tree_sitter_json::LANGUAGE.into(),
240 tree_sitter_json::HIGHLIGHTS_QUERY,
241 None,
242 );
243 r.register(
244 "html",
245 &["html", "htm"],
246 tree_sitter_html::LANGUAGE.into(),
247 tree_sitter_html::HIGHLIGHTS_QUERY,
248 tree_sitter_html::INJECTIONS_QUERY,
249 None,
250 );
251 r.add(
252 "css",
253 &["css"],
254 tree_sitter_css::LANGUAGE.into(),
255 tree_sitter_css::HIGHLIGHTS_QUERY,
256 None,
257 );
258 r.add(
259 "yaml",
260 &["yml", "yaml"],
261 tree_sitter_yaml::LANGUAGE.into(),
262 tree_sitter_yaml::HIGHLIGHTS_QUERY,
263 None,
264 );
265 r.add(
270 "sql",
271 &["sql"],
272 tree_sitter_sequel::LANGUAGE.into(),
273 format!(
274 "{}\n((literal) @number (#match? @number \"^[-+]?[0-9][0-9.]*$\"))\n",
275 tree_sitter_sequel::HIGHLIGHTS_QUERY
276 ),
277 None,
278 );
279 r.register(
283 "markdown",
284 &["md", "markdown"],
285 tree_sitter_md::LANGUAGE.into(),
286 tree_sitter_md::HIGHLIGHT_QUERY_BLOCK,
287 tree_sitter_md::INJECTION_QUERY_BLOCK,
288 None,
289 );
290 let md_inline: Language = tree_sitter_md::INLINE_LANGUAGE.into();
291 r.markdown_inline_query =
292 Query::new(&md_inline, tree_sitter_md::HIGHLIGHT_QUERY_INLINE).ok();
293 r.register(
294 "markdown_inline",
295 &[],
296 md_inline,
297 tree_sitter_md::HIGHLIGHT_QUERY_INLINE,
298 "",
299 None,
300 );
301
302 r.add(
303 "toml",
304 &["toml"],
305 tree_sitter_toml_ng::LANGUAGE.into(),
306 tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
307 None,
308 );
309 r.add(
311 "hcl",
312 &["tf", "tfvars", "hcl"],
313 tree_sitter_hcl::LANGUAGE.into(),
314 include_str!("../../queries/hcl/highlights.scm"),
315 None,
316 );
317 r.add(
318 "dockerfile",
319 &["dockerfile", "containerfile"],
320 arborium_dockerfile::language().into(),
321 arborium_dockerfile::HIGHLIGHTS_QUERY,
322 None,
323 );
324 r.add(
325 "make",
326 &["mk"],
327 tree_sitter_make::LANGUAGE.into(),
328 tree_sitter_make::HIGHLIGHTS_QUERY,
329 None,
330 );
331 r.add(
332 "lua",
333 &["lua"],
334 tree_sitter_lua::LANGUAGE.into(),
335 tree_sitter_lua::HIGHLIGHTS_QUERY,
336 Some(tree_sitter_lua::TAGS_QUERY),
337 );
338 r.add(
339 "nix",
340 &["nix"],
341 tree_sitter_nix::LANGUAGE.into(),
342 tree_sitter_nix::HIGHLIGHTS_QUERY,
343 None,
344 );
345 r.add(
346 "xml",
347 &["xml", "xsd", "xslt", "svg"],
348 tree_sitter_xml::LANGUAGE_XML.into(),
349 tree_sitter_xml::XML_HIGHLIGHT_QUERY,
350 None,
351 );
352 r.add(
353 "swift",
354 &["swift"],
355 tree_sitter_swift::LANGUAGE.into(),
356 tree_sitter_swift::HIGHLIGHTS_QUERY,
357 Some(tree_sitter_swift::TAGS_QUERY),
358 );
359 r.add(
360 "scala",
361 &["scala", "sbt"],
362 tree_sitter_scala::LANGUAGE.into(),
363 tree_sitter_scala::HIGHLIGHTS_QUERY,
364 None,
365 );
366 r.add(
367 "elixir",
368 &["ex", "exs"],
369 tree_sitter_elixir::LANGUAGE.into(),
370 tree_sitter_elixir::HIGHLIGHTS_QUERY,
371 Some(tree_sitter_elixir::TAGS_QUERY),
372 );
373 r.add(
374 "zig",
375 &["zig", "zon"],
376 tree_sitter_zig::LANGUAGE.into(),
377 tree_sitter_zig::HIGHLIGHTS_QUERY,
378 None,
379 );
380 r.add(
381 "haskell",
382 &["hs"],
383 tree_sitter_haskell::LANGUAGE.into(),
384 tree_sitter_haskell::HIGHLIGHTS_QUERY,
385 None,
386 );
387 r.add(
388 "dart",
389 &["dart"],
390 tree_sitter_dart::LANGUAGE.into(),
391 tree_sitter_dart::HIGHLIGHTS_QUERY,
392 Some(tree_sitter_dart::TAGS_QUERY),
393 );
394 r.add(
395 "powershell",
396 &["ps1", "psm1", "psd1"],
397 tree_sitter_powershell::LANGUAGE.into(),
398 tree_sitter_powershell::HIGHLIGHTS_QUERY,
399 None,
400 );
401 r.register(
402 "svelte",
403 &["svelte"],
404 tree_sitter_svelte_ng::LANGUAGE.into(),
405 format!(
408 "{}\n{}",
409 tree_sitter_html::HIGHLIGHTS_QUERY,
410 tree_sitter_svelte_ng::HIGHLIGHTS_QUERY
411 ),
412 tree_sitter_svelte_ng::INJECTIONS_QUERY,
413 None,
414 );
415 r.name_files("make", &["makefile", "gnumakefile"]);
416 r.name_files("dockerfile", &["dockerfile", "containerfile"]);
417
418 r
419 }
420
421 fn name_files(&mut self, name: &'static str, filenames: &'static [&'static str]) {
423 let Some(&idx) = self.by_name.get(name) else {
424 return;
425 };
426 for file in filenames {
427 self.by_filename.insert(file, idx);
428 }
429 }
430
431 fn add(
432 &mut self,
433 name: &'static str,
434 extensions: &'static [&'static str],
435 language: Language,
436 highlights: impl Into<Cow<'static, str>>,
437 tags: Option<&'static str>,
438 ) {
439 self.register(name, extensions, language, highlights, "", tags);
440 }
441
442 fn register(
443 &mut self,
444 name: &'static str,
445 extensions: &'static [&'static str],
446 language: Language,
447 highlights: impl Into<Cow<'static, str>>,
448 injections: impl Into<Cow<'static, str>>,
449 tags: Option<&'static str>,
450 ) {
451 let idx = self.entries.len();
452 self.entries.push(LangEntry {
453 name,
454 language,
455 highlights: highlights.into(),
456 injections: injections.into(),
457 tags_query: tags.map(Cow::Borrowed),
458 config: OnceLock::new(),
459 tags: OnceLock::new(),
460 });
461 self.by_name.insert(name, idx);
462 for ext in extensions {
463 self.by_ext.insert(ext, idx);
464 }
465 }
466
467 pub fn for_path(&self, path: &str) -> Option<&LangEntry> {
470 let name = Path::new(path).file_name()?.to_str()?.to_ascii_lowercase();
471 if let Some(&idx) = self.by_filename.get(name.as_str()) {
472 return self.entries.get(idx);
473 }
474 let ext = Path::new(path).extension()?.to_str()?;
475 let &idx = self.by_ext.get(ext)?;
476 self.entries.get(idx)
477 }
478
479 pub fn for_token(&self, token: &str) -> Option<&LangEntry> {
482 let token = token.trim().to_ascii_lowercase();
483 let token = match token.as_str() {
484 "c++" => "cpp",
485 "c#" | "csharp" => "cs",
486 "shell" => "bash",
487 "golang" => "go",
488 "terraform" => "hcl",
489 "docker" => "dockerfile",
490 "makefile" => "make",
491 "pwsh" | "ps" => "powershell",
492 other => other,
493 };
494 let &idx = self.by_name.get(token).or_else(|| self.by_ext.get(token))?;
495 self.entries.get(idx)
496 }
497
498 pub fn config_for_injection(&self, lang: &str) -> Option<&HighlightConfiguration> {
502 self.for_token(lang)?.config()
503 }
504
505 pub fn markdown_inline_spans(&self, content: &str) -> Vec<(Range<usize>, &'static str)> {
510 if content.len() > MAX_PARSE_BYTES {
511 return Vec::new();
512 }
513 let (Some(query), Some(block), Some(inline)) = (
514 self.markdown_inline_query.as_ref(),
515 self.by_name
516 .get("markdown")
517 .and_then(|&i| self.entries.get(i)),
518 self.by_name
519 .get("markdown_inline")
520 .and_then(|&i| self.entries.get(i)),
521 ) else {
522 return Vec::new();
523 };
524
525 let mut bp = Parser::new();
526 if bp.set_language(&block.language).is_err() {
527 return Vec::new();
528 }
529 let Some(block_tree) = bp.parse(content, None) else {
530 return Vec::new();
531 };
532 let ranges = inline_node_ranges(&block_tree);
533 if ranges.is_empty() {
534 return Vec::new();
535 }
536
537 let mut ip = Parser::new();
538 if ip.set_included_ranges(&ranges).is_err() || ip.set_language(&inline.language).is_err() {
539 return Vec::new();
540 }
541 let Some(inline_tree) = ip.parse(content, None) else {
542 return Vec::new();
543 };
544
545 let names = query.capture_names();
546 let mut cursor = QueryCursor::new();
547 let mut spans = Vec::new();
548 let mut matches = cursor.matches(query, inline_tree.root_node(), content.as_bytes());
549 while let Some(m) = matches.next() {
550 for cap in m.captures {
551 let cname = names.get(cap.index as usize).copied().unwrap_or("");
552 if let Some(name) = recognized_highlight(cname) {
553 spans.push((cap.node.byte_range(), name));
554 }
555 }
556 }
557 spans.sort_by_key(|(r, _)| r.end - r.start);
558 spans
559 }
560}
561
562fn inline_node_ranges(tree: &Tree) -> Vec<tree_sitter::Range> {
565 let mut ranges = Vec::new();
566 let mut cursor = tree.walk();
567 loop {
568 let node = cursor.node();
569 if node.kind() == "inline" && node.end_byte() > node.start_byte() {
570 ranges.push(node.range());
571 }
572 if cursor.goto_first_child() {
573 continue;
574 }
575 loop {
576 if cursor.goto_next_sibling() {
577 break;
578 }
579 if !cursor.goto_parent() {
580 return ranges;
581 }
582 }
583 }
584}
585
586fn recognized_highlight(capture: &str) -> Option<&'static str> {
589 HIGHLIGHT_NAMES
590 .iter()
591 .copied()
592 .filter(|name| {
593 capture == *name
594 || capture
595 .strip_prefix(name)
596 .is_some_and(|r| r.starts_with('.'))
597 })
598 .max_by_key(|name| name.len())
599}
600
601impl Default for LanguageRegistry {
602 fn default() -> Self {
603 Self::build()
604 }
605}