1use std::borrow::Cow;
5use std::collections::HashMap;
6use std::ops::Range;
7use std::path::Path;
8use std::sync::{LazyLock, OnceLock};
9
10use tree_sitter::{Language, Parser, Query, QueryCursor, StreamingIterator, Tree};
11use tree_sitter_highlight::HighlightConfiguration;
12
13use crate::syntax::MAX_PARSE_BYTES;
14
15pub const HIGHLIGHT_NAMES: &[&str] = &[
18 "attribute",
19 "boolean",
20 "comment",
21 "conditional",
22 "constant",
23 "constant.builtin",
24 "constructor",
25 "field",
26 "function",
27 "function.builtin",
28 "keyword",
29 "label",
30 "number",
31 "operator",
32 "parameter",
33 "property",
34 "punctuation",
35 "punctuation.bracket",
36 "punctuation.delimiter",
37 "spell",
38 "storageclass",
39 "string",
40 "string.escape",
41 "string.special",
42 "tag",
43 "text.emphasis",
44 "text.literal",
45 "text.reference",
46 "text.strong",
47 "text.title",
48 "text.uri",
49 "type",
50 "type.builtin",
51 "variable",
52 "variable.builtin",
53 "variable.parameter",
54];
55
56pub struct LangEntry {
57 pub name: &'static str,
58 pub language: Language,
59 highlights: Cow<'static, str>,
60 injections: Cow<'static, str>,
61 tags_query: Option<Cow<'static, str>>,
62 pub layout_significant: bool,
64 config: OnceLock<Option<HighlightConfiguration>>,
67 tags: OnceLock<Option<Query>>,
68}
69
70impl LangEntry {
71 pub fn config(&self) -> Option<&HighlightConfiguration> {
72 self.config
73 .get_or_init(|| {
74 HighlightConfiguration::new(
75 self.language.clone(),
76 self.name,
77 &self.highlights,
78 &self.injections,
79 "",
80 )
81 .ok()
82 .map(|mut config| {
83 config.configure(HIGHLIGHT_NAMES);
84 config
85 })
86 })
87 .as_ref()
88 }
89
90 pub fn tags(&self) -> Option<&Query> {
91 self.tags
92 .get_or_init(|| Query::new(&self.language, self.tags_query.as_deref()?).ok())
93 .as_ref()
94 }
95}
96
97pub static REGISTRY: LazyLock<LanguageRegistry> = LazyLock::new(LanguageRegistry::build);
98
99const TEMPLATE_SUFFIXES: &[&str] = &[
101 "example", "sample", "template", "tmpl", "tpl", "dist", "default", "local", "orig", "bak",
102];
103
104pub struct LanguageRegistry {
105 entries: Vec<LangEntry>,
106 by_ext: HashMap<&'static str, usize>,
107 by_name: HashMap<&'static str, usize>,
108 by_filename: HashMap<&'static str, usize>,
109 markdown_inline_query: Option<Query>,
112}
113
114impl LanguageRegistry {
115 #[allow(clippy::too_many_lines)]
117 pub fn build() -> Self {
118 let mut r = Self {
119 entries: Vec::new(),
120 by_ext: HashMap::new(),
121 by_name: HashMap::new(),
122 by_filename: HashMap::new(),
123 markdown_inline_query: None,
124 };
125
126 r.add(
127 "rust",
128 &["rs"],
129 tree_sitter_rust::LANGUAGE.into(),
130 tree_sitter_rust::HIGHLIGHTS_QUERY,
131 Some(tree_sitter_rust::TAGS_QUERY),
132 );
133 r.add(
134 "python",
135 &["py", "pyi"],
136 tree_sitter_python::LANGUAGE.into(),
137 tree_sitter_python::HIGHLIGHTS_QUERY,
138 Some(tree_sitter_python::TAGS_QUERY),
139 );
140 r.layout_significant();
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 None,
162 );
163 r.tags_over_javascript(tree_sitter_typescript::TAGS_QUERY);
164 r.add(
165 "tsx",
166 &["tsx"],
167 tree_sitter_typescript::LANGUAGE_TSX.into(),
168 format!(
169 "{}\n{}\n{}",
170 tree_sitter_javascript::HIGHLIGHT_QUERY,
171 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
172 tree_sitter_typescript::HIGHLIGHTS_QUERY
173 ),
174 None,
175 );
176 r.tags_over_javascript(tree_sitter_typescript::TAGS_QUERY);
177 r.add(
178 "go",
179 &["go"],
180 tree_sitter_go::LANGUAGE.into(),
181 tree_sitter_go::HIGHLIGHTS_QUERY,
182 Some(tree_sitter_go::TAGS_QUERY),
183 );
184 r.add(
185 "c",
186 &["c", "h"],
187 tree_sitter_c::LANGUAGE.into(),
188 tree_sitter_c::HIGHLIGHT_QUERY,
189 Some(tree_sitter_c::TAGS_QUERY),
190 );
191 r.add(
192 "cpp",
193 &["cpp", "cc", "cxx", "hpp", "hh", "hxx"],
194 tree_sitter_cpp::LANGUAGE.into(),
195 format!(
197 "{}\n{}",
198 tree_sitter_c::HIGHLIGHT_QUERY,
199 tree_sitter_cpp::HIGHLIGHT_QUERY
200 ),
201 Some(tree_sitter_cpp::TAGS_QUERY),
202 );
203 r.add(
204 "java",
205 &["java"],
206 tree_sitter_java::LANGUAGE.into(),
207 tree_sitter_java::HIGHLIGHTS_QUERY,
208 Some(tree_sitter_java::TAGS_QUERY),
209 );
210 r.add(
211 "c-sharp",
212 &["cs"],
213 tree_sitter_c_sharp::LANGUAGE.into(),
214 tree_sitter_c_sharp::HIGHLIGHTS_QUERY,
215 Some(tree_sitter_c_sharp::TAGS_QUERY),
216 );
217 r.add(
218 "ruby",
219 &["rb"],
220 tree_sitter_ruby::LANGUAGE.into(),
221 tree_sitter_ruby::HIGHLIGHTS_QUERY,
222 Some(tree_sitter_ruby::TAGS_QUERY),
223 );
224 r.add(
225 "php",
226 &["php"],
227 tree_sitter_php::LANGUAGE_PHP.into(),
228 tree_sitter_php::HIGHLIGHTS_QUERY,
229 Some(tree_sitter_php::TAGS_QUERY),
230 );
231 r.add(
232 "bash",
233 &["sh", "bash", "zsh", "env"],
234 tree_sitter_bash::LANGUAGE.into(),
235 tree_sitter_bash::HIGHLIGHT_QUERY,
236 None,
237 );
238 r.add(
239 "json",
240 &["json"],
241 tree_sitter_json::LANGUAGE.into(),
242 tree_sitter_json::HIGHLIGHTS_QUERY,
243 None,
244 );
245 r.register(
246 "html",
247 &["html", "htm"],
248 tree_sitter_html::LANGUAGE.into(),
249 tree_sitter_html::HIGHLIGHTS_QUERY,
250 tree_sitter_html::INJECTIONS_QUERY,
251 None,
252 );
253 r.add(
254 "css",
255 &["css"],
256 tree_sitter_css::LANGUAGE.into(),
257 tree_sitter_css::HIGHLIGHTS_QUERY,
258 None,
259 );
260 r.add(
261 "yaml",
262 &["yml", "yaml"],
263 tree_sitter_yaml::LANGUAGE.into(),
264 tree_sitter_yaml::HIGHLIGHTS_QUERY,
265 None,
266 );
267 r.layout_significant();
268 r.add(
272 "sql",
273 &["sql"],
274 tree_sitter_sequel::LANGUAGE.into(),
275 format!(
276 "{}\n((literal) @number (#match? @number \"^[-+]?[0-9][0-9.]*$\"))\n",
277 tree_sitter_sequel::HIGHLIGHTS_QUERY
278 ),
279 None,
280 );
281 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.layout_significant();
332 r.add(
333 "lua",
334 &["lua"],
335 tree_sitter_lua::LANGUAGE.into(),
336 tree_sitter_lua::HIGHLIGHTS_QUERY,
337 Some(tree_sitter_lua::TAGS_QUERY),
338 );
339 r.add(
340 "nix",
341 &["nix"],
342 tree_sitter_nix::LANGUAGE.into(),
343 tree_sitter_nix::HIGHLIGHTS_QUERY,
344 None,
345 );
346 r.add(
347 "xml",
348 &["xml", "xsd", "xslt", "svg"],
349 tree_sitter_xml::LANGUAGE_XML.into(),
350 tree_sitter_xml::XML_HIGHLIGHT_QUERY,
351 None,
352 );
353 r.add(
354 "swift",
355 &["swift"],
356 tree_sitter_swift::LANGUAGE.into(),
357 tree_sitter_swift::HIGHLIGHTS_QUERY,
358 Some(tree_sitter_swift::TAGS_QUERY),
359 );
360 r.add(
361 "scala",
362 &["scala", "sbt"],
363 tree_sitter_scala::LANGUAGE.into(),
364 tree_sitter_scala::HIGHLIGHTS_QUERY,
365 None,
366 );
367 r.layout_significant();
368 r.add(
369 "elixir",
370 &["ex", "exs"],
371 tree_sitter_elixir::LANGUAGE.into(),
372 tree_sitter_elixir::HIGHLIGHTS_QUERY,
373 Some(tree_sitter_elixir::TAGS_QUERY),
374 );
375 r.add(
376 "zig",
377 &["zig", "zon"],
378 tree_sitter_zig::LANGUAGE.into(),
379 tree_sitter_zig::HIGHLIGHTS_QUERY,
380 None,
381 );
382 r.add(
383 "haskell",
384 &["hs"],
385 tree_sitter_haskell::LANGUAGE.into(),
386 tree_sitter_haskell::HIGHLIGHTS_QUERY,
387 None,
388 );
389 r.layout_significant();
390 r.add(
391 "dart",
392 &["dart"],
393 tree_sitter_dart::LANGUAGE.into(),
394 tree_sitter_dart::HIGHLIGHTS_QUERY,
395 Some(tree_sitter_dart::TAGS_QUERY),
396 );
397 r.add(
398 "powershell",
399 &["ps1", "psm1", "psd1"],
400 tree_sitter_powershell::LANGUAGE.into(),
401 tree_sitter_powershell::HIGHLIGHTS_QUERY,
402 None,
403 );
404 r.register(
405 "svelte",
406 &["svelte"],
407 tree_sitter_svelte_ng::LANGUAGE.into(),
408 format!(
411 "{}\n{}",
412 tree_sitter_html::HIGHLIGHTS_QUERY,
413 tree_sitter_svelte_ng::HIGHLIGHTS_QUERY
414 ),
415 tree_sitter_svelte_ng::INJECTIONS_QUERY,
416 None,
417 );
418 r.name_files("make", &["makefile", "gnumakefile"]);
419 r.name_files("dockerfile", &["dockerfile", "containerfile"]);
420 r.name_files(
421 "bash",
422 &[
423 ".env",
424 ".envrc",
425 ".bashrc",
426 ".bash_profile",
427 ".bash_aliases",
428 ".bash_logout",
429 ".profile",
430 ".zshrc",
431 ".zprofile",
432 ".zshenv",
433 ".zlogin",
434 ],
435 );
436
437 r
438 }
439
440 fn name_files(&mut self, name: &'static str, filenames: &'static [&'static str]) {
441 let Some(&idx) = self.by_name.get(name) else {
442 return;
443 };
444 for file in filenames {
445 self.by_filename.insert(file, idx);
446 }
447 }
448
449 fn add(
450 &mut self,
451 name: &'static str,
452 extensions: &'static [&'static str],
453 language: Language,
454 highlights: impl Into<Cow<'static, str>>,
455 tags: Option<&'static str>,
456 ) {
457 self.register(name, extensions, language, highlights, "", tags);
458 }
459
460 fn register(
461 &mut self,
462 name: &'static str,
463 extensions: &'static [&'static str],
464 language: Language,
465 highlights: impl Into<Cow<'static, str>>,
466 injections: impl Into<Cow<'static, str>>,
467 tags: Option<&'static str>,
468 ) {
469 let idx = self.entries.len();
470 self.entries.push(LangEntry {
471 name,
472 language,
473 highlights: highlights.into(),
474 injections: injections.into(),
475 tags_query: tags.map(Cow::Borrowed),
476 layout_significant: false,
477 config: OnceLock::new(),
478 tags: OnceLock::new(),
479 });
480 self.by_name.insert(name, idx);
481 for ext in extensions {
482 self.by_ext.insert(ext, idx);
483 }
484 }
485
486 fn layout_significant(&mut self) {
488 if let Some(entry) = self.entries.last_mut() {
489 entry.layout_significant = true;
490 }
491 }
492
493 fn tags_over_javascript(&mut self, tags: &'static str) {
496 if let Some(entry) = self.entries.last_mut() {
497 entry.tags_query = Some(Cow::Owned(format!(
498 "{}\n{tags}",
499 tree_sitter_javascript::TAGS_QUERY
500 )));
501 }
502 }
503
504 pub fn for_path(&self, path: &str) -> Option<&LangEntry> {
506 let name = Path::new(path).file_name()?.to_str()?.to_ascii_lowercase();
507 if let Some(&idx) = self.by_filename.get(name.as_str()) {
508 return self.entries.get(idx);
509 }
510 let ext = Path::new(path).extension()?.to_str()?;
511 let &idx = self.by_ext.get(ext)?;
512 self.entries.get(idx)
513 }
514
515 pub fn for_file(&self, path: &str, content: &str) -> Option<&LangEntry> {
517 self.for_path(path)
518 .or_else(|| {
519 let name = Path::new(path).file_name()?.to_str()?;
520 self.by_convention(&name.to_ascii_lowercase())
521 })
522 .or_else(|| self.by_name(shebang_language(content.lines().next()?)?))
523 }
524
525 fn by_convention(&self, name: &str) -> Option<&LangEntry> {
528 let prefix = name
529 .get(1..)
530 .and_then(|rest| rest.find('.'))
531 .and_then(|dot| name.get(..=dot));
532 if let Some(&idx) = prefix.and_then(|prefix| self.by_filename.get(prefix)) {
533 return self.entries.get(idx);
534 }
535 let (stem, suffix) = name.rsplit_once('.')?;
536 if !TEMPLATE_SUFFIXES.contains(&suffix) {
537 return None;
538 }
539 self.for_path(stem).or_else(|| self.by_convention(stem))
540 }
541
542 pub fn by_name(&self, name: &str) -> Option<&LangEntry> {
543 let &idx = self.by_name.get(name)?;
544 self.entries.get(idx)
545 }
546
547 pub fn names(&self) -> Vec<&'static str> {
548 let mut names: Vec<&'static str> = self.entries.iter().map(|entry| entry.name).collect();
549 names.sort_unstable();
550 names
551 }
552
553 pub fn for_token(&self, token: &str) -> Option<&LangEntry> {
555 let token = token.trim().to_ascii_lowercase();
556 let token = match token.as_str() {
557 "c++" => "cpp",
558 "c#" | "csharp" => "cs",
559 "shell" => "bash",
560 "golang" => "go",
561 "terraform" => "hcl",
562 "docker" => "dockerfile",
563 "makefile" => "make",
564 "pwsh" | "ps" => "powershell",
565 other => other,
566 };
567 let &idx = self.by_name.get(token).or_else(|| self.by_ext.get(token))?;
568 self.entries.get(idx)
569 }
570
571 pub fn config_for_injection(&self, lang: &str) -> Option<&HighlightConfiguration> {
572 self.for_token(lang)?.config()
573 }
574
575 pub fn markdown_inline_spans(&self, content: &str) -> Vec<(Range<usize>, &'static str)> {
578 if content.len() > MAX_PARSE_BYTES {
579 return Vec::new();
580 }
581 let (Some(query), Some(block), Some(inline)) = (
582 self.markdown_inline_query.as_ref(),
583 self.by_name
584 .get("markdown")
585 .and_then(|&i| self.entries.get(i)),
586 self.by_name
587 .get("markdown_inline")
588 .and_then(|&i| self.entries.get(i)),
589 ) else {
590 return Vec::new();
591 };
592
593 let mut bp = Parser::new();
594 if bp.set_language(&block.language).is_err() {
595 return Vec::new();
596 }
597 let Some(block_tree) = bp.parse(content, None) else {
598 return Vec::new();
599 };
600 let ranges = inline_node_ranges(&block_tree);
601 if ranges.is_empty() {
602 return Vec::new();
603 }
604
605 let mut ip = Parser::new();
606 if ip.set_included_ranges(&ranges).is_err() || ip.set_language(&inline.language).is_err() {
607 return Vec::new();
608 }
609 let Some(inline_tree) = ip.parse(content, None) else {
610 return Vec::new();
611 };
612
613 let names = query.capture_names();
614 let mut cursor = QueryCursor::new();
615 let mut spans = Vec::new();
616 let mut matches = cursor.matches(query, inline_tree.root_node(), content.as_bytes());
617 while let Some(m) = matches.next() {
618 for cap in m.captures {
619 let cname = names.get(cap.index as usize).copied().unwrap_or("");
620 if let Some(name) = recognized_highlight(cname) {
621 spans.push((cap.node.byte_range(), name));
622 }
623 }
624 }
625 spans.sort_by_key(|(r, _)| r.end - r.start);
626 spans
627 }
628}
629
630fn inline_node_ranges(tree: &Tree) -> Vec<tree_sitter::Range> {
631 let mut ranges = Vec::new();
632 let mut cursor = tree.walk();
633 loop {
634 let node = cursor.node();
635 if node.kind() == "inline" && node.end_byte() > node.start_byte() {
636 ranges.push(node.range());
637 }
638 if cursor.goto_first_child() {
639 continue;
640 }
641 loop {
642 if cursor.goto_next_sibling() {
643 break;
644 }
645 if !cursor.goto_parent() {
646 return ranges;
647 }
648 }
649 }
650}
651
652fn recognized_highlight(capture: &str) -> Option<&'static str> {
655 HIGHLIGHT_NAMES
656 .iter()
657 .copied()
658 .filter(|name| {
659 capture == *name
660 || capture
661 .strip_prefix(name)
662 .is_some_and(|r| r.starts_with('.'))
663 })
664 .max_by_key(|name| name.len())
665}
666
667impl Default for LanguageRegistry {
668 fn default() -> Self {
669 Self::build()
670 }
671}
672
673fn shebang_language(line: &str) -> Option<&'static str> {
674 let command = line.strip_prefix("#!")?.trim();
675 let mut words = command.split_whitespace();
676 let program = words.next()?;
677 let program = if program.ends_with("/env") {
678 words.find(|word| !word.starts_with('-'))?
679 } else {
680 program
681 };
682 let name = program.rsplit('/').next()?;
683 let name = name.trim_end_matches(|c: char| c.is_ascii_digit() || c == '.');
684 let lang = match name {
685 "python" | "pypy" => "python",
686 "bash" | "sh" | "zsh" | "dash" | "ksh" | "ash" => "bash",
687 "node" | "nodejs" | "deno" | "bun" => "javascript",
688 "ruby" => "ruby",
689 "php" => "php",
690 "lua" | "luajit" => "lua",
691 "pwsh" => "powershell",
692 "elixir" => "elixir",
693 _ => return None,
694 };
695 Some(lang)
696}
697
698#[cfg(test)]
699mod tests {
700 use super::REGISTRY;
701
702 fn lang(path: &str) -> Option<&'static str> {
703 REGISTRY.for_file(path, "").map(|entry| entry.name)
704 }
705
706 #[test]
707 fn convention_names_pick_their_grammar() {
708 assert_eq!(lang("deploy/Dockerfile.prod"), Some("dockerfile"));
709 assert_eq!(lang("api.dockerfile"), Some("dockerfile"));
710 assert_eq!(lang(".env"), Some("bash"));
711 assert_eq!(lang("app/.env.example"), Some("bash"));
712 assert_eq!(lang("Makefile.local"), Some("make"));
713 assert_eq!(lang(".zshrc"), Some("bash"));
714 }
715
716 #[test]
717 fn a_template_suffix_falls_back_to_the_real_extension() {
718 assert_eq!(lang("config.yml.example"), Some("yaml"));
719 assert_eq!(lang("settings.json.dist"), Some("json"));
720 assert_eq!(lang("notes.example"), None, "nothing under the suffix");
721 assert_eq!(lang(".env.example"), Some("bash"));
722 assert_eq!(
723 REGISTRY.for_path("config.yml.example").map(|e| e.name),
724 None,
725 "the file kinds and Stats read the name and extension alone"
726 );
727 }
728
729 #[test]
730 fn a_shebang_names_the_language_of_a_script_with_no_extension() {
731 let script = "#!/usr/bin/env python3\nprint(1)\n";
732 assert_eq!(
733 REGISTRY.for_file("bin/tool", script).map(|e| e.name),
734 Some("python")
735 );
736 let shell = "#!/bin/sh -e\necho hi\n";
737 assert_eq!(
738 REGISTRY.for_file("run", shell).map(|e| e.name),
739 Some("bash")
740 );
741 assert_eq!(
742 REGISTRY.for_file("README", "plain text\n").map(|e| e.name),
743 None
744 );
745 }
746
747 #[test]
748 fn every_interpreter_a_shebang_names_is_a_bundled_grammar() {
749 let inferred = [
750 "python", "sh", "node", "ruby", "php", "lua", "pwsh", "elixir",
751 ]
752 .into_iter()
753 .filter_map(|program| super::shebang_language(&format!("#!/bin/{program}")));
754 for name in inferred {
755 assert!(REGISTRY.by_name(name).is_some(), "{name} is no grammar");
756 }
757 }
758
759 #[test]
760 fn exactly_the_layout_languages_are_layout_significant() {
761 let flagged: Vec<_> = super::REGISTRY
762 .entries
763 .iter()
764 .filter(|entry| entry.layout_significant)
765 .map(|entry| entry.name)
766 .collect();
767 assert_eq!(flagged, ["python", "yaml", "make", "scala", "haskell"]);
768 }
769}