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 pub layout_significant: bool,
68 config: OnceLock<Option<HighlightConfiguration>>,
72 tags: OnceLock<Option<Query>>,
73}
74
75impl LangEntry {
76 pub fn config(&self) -> Option<&HighlightConfiguration> {
77 self.config
78 .get_or_init(|| {
79 HighlightConfiguration::new(
80 self.language.clone(),
81 self.name,
82 &self.highlights,
83 &self.injections,
84 "",
85 )
86 .ok()
87 .map(|mut config| {
88 config.configure(HIGHLIGHT_NAMES);
89 config
90 })
91 })
92 .as_ref()
93 }
94
95 pub fn tags(&self) -> Option<&Query> {
96 self.tags
97 .get_or_init(|| Query::new(&self.language, self.tags_query.as_deref()?).ok())
98 .as_ref()
99 }
100}
101
102pub static REGISTRY: LazyLock<LanguageRegistry> = LazyLock::new(LanguageRegistry::build);
106
107pub struct LanguageRegistry {
108 entries: Vec<LangEntry>,
109 by_ext: HashMap<&'static str, usize>,
110 by_name: HashMap<&'static str, usize>,
111 by_filename: HashMap<&'static str, usize>,
112 markdown_inline_query: Option<Query>,
116}
117
118impl LanguageRegistry {
119 #[allow(clippy::too_many_lines)]
122 pub fn build() -> Self {
123 let mut r = Self {
124 entries: Vec::new(),
125 by_ext: HashMap::new(),
126 by_name: HashMap::new(),
127 by_filename: HashMap::new(),
128 markdown_inline_query: None,
129 };
130
131 r.add(
132 "rust",
133 &["rs"],
134 tree_sitter_rust::LANGUAGE.into(),
135 tree_sitter_rust::HIGHLIGHTS_QUERY,
136 Some(tree_sitter_rust::TAGS_QUERY),
137 );
138 r.add(
139 "python",
140 &["py", "pyi"],
141 tree_sitter_python::LANGUAGE.into(),
142 tree_sitter_python::HIGHLIGHTS_QUERY,
143 Some(tree_sitter_python::TAGS_QUERY),
144 );
145 r.layout_significant();
146 r.add(
147 "javascript",
148 &["js", "jsx", "mjs", "cjs"],
149 tree_sitter_javascript::LANGUAGE.into(),
150 format!(
151 "{}\n{}",
152 tree_sitter_javascript::HIGHLIGHT_QUERY,
153 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY
154 ),
155 Some(tree_sitter_javascript::TAGS_QUERY),
156 );
157 r.add(
158 "typescript",
159 &["ts", "mts", "cts"],
160 tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
161 format!(
162 "{}\n{}",
163 tree_sitter_javascript::HIGHLIGHT_QUERY,
164 tree_sitter_typescript::HIGHLIGHTS_QUERY
165 ),
166 None,
167 );
168 r.tags_over_javascript(tree_sitter_typescript::TAGS_QUERY);
169 r.add(
170 "tsx",
171 &["tsx"],
172 tree_sitter_typescript::LANGUAGE_TSX.into(),
173 format!(
174 "{}\n{}\n{}",
175 tree_sitter_javascript::HIGHLIGHT_QUERY,
176 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
177 tree_sitter_typescript::HIGHLIGHTS_QUERY
178 ),
179 None,
180 );
181 r.tags_over_javascript(tree_sitter_typescript::TAGS_QUERY);
182 r.add(
183 "go",
184 &["go"],
185 tree_sitter_go::LANGUAGE.into(),
186 tree_sitter_go::HIGHLIGHTS_QUERY,
187 Some(tree_sitter_go::TAGS_QUERY),
188 );
189 r.add(
190 "c",
191 &["c", "h"],
192 tree_sitter_c::LANGUAGE.into(),
193 tree_sitter_c::HIGHLIGHT_QUERY,
194 Some(tree_sitter_c::TAGS_QUERY),
195 );
196 r.add(
197 "cpp",
198 &["cpp", "cc", "cxx", "hpp", "hh", "hxx"],
199 tree_sitter_cpp::LANGUAGE.into(),
200 format!(
202 "{}\n{}",
203 tree_sitter_c::HIGHLIGHT_QUERY,
204 tree_sitter_cpp::HIGHLIGHT_QUERY
205 ),
206 Some(tree_sitter_cpp::TAGS_QUERY),
207 );
208 r.add(
209 "java",
210 &["java"],
211 tree_sitter_java::LANGUAGE.into(),
212 tree_sitter_java::HIGHLIGHTS_QUERY,
213 Some(tree_sitter_java::TAGS_QUERY),
214 );
215 r.add(
216 "c-sharp",
217 &["cs"],
218 tree_sitter_c_sharp::LANGUAGE.into(),
219 tree_sitter_c_sharp::HIGHLIGHTS_QUERY,
220 Some(tree_sitter_c_sharp::TAGS_QUERY),
221 );
222 r.add(
223 "ruby",
224 &["rb"],
225 tree_sitter_ruby::LANGUAGE.into(),
226 tree_sitter_ruby::HIGHLIGHTS_QUERY,
227 Some(tree_sitter_ruby::TAGS_QUERY),
228 );
229 r.add(
230 "php",
231 &["php"],
232 tree_sitter_php::LANGUAGE_PHP.into(),
233 tree_sitter_php::HIGHLIGHTS_QUERY,
234 Some(tree_sitter_php::TAGS_QUERY),
235 );
236 r.add(
237 "bash",
238 &["sh", "bash", "zsh"],
239 tree_sitter_bash::LANGUAGE.into(),
240 tree_sitter_bash::HIGHLIGHT_QUERY,
241 None,
242 );
243 r.add(
244 "json",
245 &["json"],
246 tree_sitter_json::LANGUAGE.into(),
247 tree_sitter_json::HIGHLIGHTS_QUERY,
248 None,
249 );
250 r.register(
251 "html",
252 &["html", "htm"],
253 tree_sitter_html::LANGUAGE.into(),
254 tree_sitter_html::HIGHLIGHTS_QUERY,
255 tree_sitter_html::INJECTIONS_QUERY,
256 None,
257 );
258 r.add(
259 "css",
260 &["css"],
261 tree_sitter_css::LANGUAGE.into(),
262 tree_sitter_css::HIGHLIGHTS_QUERY,
263 None,
264 );
265 r.add(
266 "yaml",
267 &["yml", "yaml"],
268 tree_sitter_yaml::LANGUAGE.into(),
269 tree_sitter_yaml::HIGHLIGHTS_QUERY,
270 None,
271 );
272 r.layout_significant();
273 r.add(
278 "sql",
279 &["sql"],
280 tree_sitter_sequel::LANGUAGE.into(),
281 format!(
282 "{}\n((literal) @number (#match? @number \"^[-+]?[0-9][0-9.]*$\"))\n",
283 tree_sitter_sequel::HIGHLIGHTS_QUERY
284 ),
285 None,
286 );
287 r.register(
291 "markdown",
292 &["md", "markdown"],
293 tree_sitter_md::LANGUAGE.into(),
294 tree_sitter_md::HIGHLIGHT_QUERY_BLOCK,
295 tree_sitter_md::INJECTION_QUERY_BLOCK,
296 None,
297 );
298 let md_inline: Language = tree_sitter_md::INLINE_LANGUAGE.into();
299 r.markdown_inline_query =
300 Query::new(&md_inline, tree_sitter_md::HIGHLIGHT_QUERY_INLINE).ok();
301 r.register(
302 "markdown_inline",
303 &[],
304 md_inline,
305 tree_sitter_md::HIGHLIGHT_QUERY_INLINE,
306 "",
307 None,
308 );
309
310 r.add(
311 "toml",
312 &["toml"],
313 tree_sitter_toml_ng::LANGUAGE.into(),
314 tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
315 None,
316 );
317 r.add(
319 "hcl",
320 &["tf", "tfvars", "hcl"],
321 tree_sitter_hcl::LANGUAGE.into(),
322 include_str!("../../queries/hcl/highlights.scm"),
323 None,
324 );
325 r.add(
326 "dockerfile",
327 &["dockerfile", "containerfile"],
328 arborium_dockerfile::language().into(),
329 arborium_dockerfile::HIGHLIGHTS_QUERY,
330 None,
331 );
332 r.add(
333 "make",
334 &["mk"],
335 tree_sitter_make::LANGUAGE.into(),
336 tree_sitter_make::HIGHLIGHTS_QUERY,
337 None,
338 );
339 r.layout_significant();
340 r.add(
341 "lua",
342 &["lua"],
343 tree_sitter_lua::LANGUAGE.into(),
344 tree_sitter_lua::HIGHLIGHTS_QUERY,
345 Some(tree_sitter_lua::TAGS_QUERY),
346 );
347 r.add(
348 "nix",
349 &["nix"],
350 tree_sitter_nix::LANGUAGE.into(),
351 tree_sitter_nix::HIGHLIGHTS_QUERY,
352 None,
353 );
354 r.add(
355 "xml",
356 &["xml", "xsd", "xslt", "svg"],
357 tree_sitter_xml::LANGUAGE_XML.into(),
358 tree_sitter_xml::XML_HIGHLIGHT_QUERY,
359 None,
360 );
361 r.add(
362 "swift",
363 &["swift"],
364 tree_sitter_swift::LANGUAGE.into(),
365 tree_sitter_swift::HIGHLIGHTS_QUERY,
366 Some(tree_sitter_swift::TAGS_QUERY),
367 );
368 r.add(
369 "scala",
370 &["scala", "sbt"],
371 tree_sitter_scala::LANGUAGE.into(),
372 tree_sitter_scala::HIGHLIGHTS_QUERY,
373 None,
374 );
375 r.layout_significant();
376 r.add(
377 "elixir",
378 &["ex", "exs"],
379 tree_sitter_elixir::LANGUAGE.into(),
380 tree_sitter_elixir::HIGHLIGHTS_QUERY,
381 Some(tree_sitter_elixir::TAGS_QUERY),
382 );
383 r.add(
384 "zig",
385 &["zig", "zon"],
386 tree_sitter_zig::LANGUAGE.into(),
387 tree_sitter_zig::HIGHLIGHTS_QUERY,
388 None,
389 );
390 r.add(
391 "haskell",
392 &["hs"],
393 tree_sitter_haskell::LANGUAGE.into(),
394 tree_sitter_haskell::HIGHLIGHTS_QUERY,
395 None,
396 );
397 r.layout_significant();
398 r.add(
399 "dart",
400 &["dart"],
401 tree_sitter_dart::LANGUAGE.into(),
402 tree_sitter_dart::HIGHLIGHTS_QUERY,
403 Some(tree_sitter_dart::TAGS_QUERY),
404 );
405 r.add(
406 "powershell",
407 &["ps1", "psm1", "psd1"],
408 tree_sitter_powershell::LANGUAGE.into(),
409 tree_sitter_powershell::HIGHLIGHTS_QUERY,
410 None,
411 );
412 r.register(
413 "svelte",
414 &["svelte"],
415 tree_sitter_svelte_ng::LANGUAGE.into(),
416 format!(
419 "{}\n{}",
420 tree_sitter_html::HIGHLIGHTS_QUERY,
421 tree_sitter_svelte_ng::HIGHLIGHTS_QUERY
422 ),
423 tree_sitter_svelte_ng::INJECTIONS_QUERY,
424 None,
425 );
426 r.name_files("make", &["makefile", "gnumakefile"]);
427 r.name_files("dockerfile", &["dockerfile", "containerfile"]);
428
429 r
430 }
431
432 fn name_files(&mut self, name: &'static str, filenames: &'static [&'static str]) {
434 let Some(&idx) = self.by_name.get(name) else {
435 return;
436 };
437 for file in filenames {
438 self.by_filename.insert(file, idx);
439 }
440 }
441
442 fn add(
443 &mut self,
444 name: &'static str,
445 extensions: &'static [&'static str],
446 language: Language,
447 highlights: impl Into<Cow<'static, str>>,
448 tags: Option<&'static str>,
449 ) {
450 self.register(name, extensions, language, highlights, "", tags);
451 }
452
453 fn register(
454 &mut self,
455 name: &'static str,
456 extensions: &'static [&'static str],
457 language: Language,
458 highlights: impl Into<Cow<'static, str>>,
459 injections: impl Into<Cow<'static, str>>,
460 tags: Option<&'static str>,
461 ) {
462 let idx = self.entries.len();
463 self.entries.push(LangEntry {
464 name,
465 language,
466 highlights: highlights.into(),
467 injections: injections.into(),
468 tags_query: tags.map(Cow::Borrowed),
469 layout_significant: false,
470 config: OnceLock::new(),
471 tags: OnceLock::new(),
472 });
473 self.by_name.insert(name, idx);
474 for ext in extensions {
475 self.by_ext.insert(ext, idx);
476 }
477 }
478
479 fn layout_significant(&mut self) {
483 if let Some(entry) = self.entries.last_mut() {
484 entry.layout_significant = true;
485 }
486 }
487
488 fn tags_over_javascript(&mut self, tags: &'static str) {
492 if let Some(entry) = self.entries.last_mut() {
493 entry.tags_query = Some(Cow::Owned(format!(
494 "{}\n{tags}",
495 tree_sitter_javascript::TAGS_QUERY
496 )));
497 }
498 }
499
500 pub fn for_path(&self, path: &str) -> Option<&LangEntry> {
503 let name = Path::new(path).file_name()?.to_str()?.to_ascii_lowercase();
504 if let Some(&idx) = self.by_filename.get(name.as_str()) {
505 return self.entries.get(idx);
506 }
507 let ext = Path::new(path).extension()?.to_str()?;
508 let &idx = self.by_ext.get(ext)?;
509 self.entries.get(idx)
510 }
511
512 pub fn for_token(&self, token: &str) -> Option<&LangEntry> {
515 let token = token.trim().to_ascii_lowercase();
516 let token = match token.as_str() {
517 "c++" => "cpp",
518 "c#" | "csharp" => "cs",
519 "shell" => "bash",
520 "golang" => "go",
521 "terraform" => "hcl",
522 "docker" => "dockerfile",
523 "makefile" => "make",
524 "pwsh" | "ps" => "powershell",
525 other => other,
526 };
527 let &idx = self.by_name.get(token).or_else(|| self.by_ext.get(token))?;
528 self.entries.get(idx)
529 }
530
531 pub fn config_for_injection(&self, lang: &str) -> Option<&HighlightConfiguration> {
535 self.for_token(lang)?.config()
536 }
537
538 pub fn markdown_inline_spans(&self, content: &str) -> Vec<(Range<usize>, &'static str)> {
543 if content.len() > MAX_PARSE_BYTES {
544 return Vec::new();
545 }
546 let (Some(query), Some(block), Some(inline)) = (
547 self.markdown_inline_query.as_ref(),
548 self.by_name
549 .get("markdown")
550 .and_then(|&i| self.entries.get(i)),
551 self.by_name
552 .get("markdown_inline")
553 .and_then(|&i| self.entries.get(i)),
554 ) else {
555 return Vec::new();
556 };
557
558 let mut bp = Parser::new();
559 if bp.set_language(&block.language).is_err() {
560 return Vec::new();
561 }
562 let Some(block_tree) = bp.parse(content, None) else {
563 return Vec::new();
564 };
565 let ranges = inline_node_ranges(&block_tree);
566 if ranges.is_empty() {
567 return Vec::new();
568 }
569
570 let mut ip = Parser::new();
571 if ip.set_included_ranges(&ranges).is_err() || ip.set_language(&inline.language).is_err() {
572 return Vec::new();
573 }
574 let Some(inline_tree) = ip.parse(content, None) else {
575 return Vec::new();
576 };
577
578 let names = query.capture_names();
579 let mut cursor = QueryCursor::new();
580 let mut spans = Vec::new();
581 let mut matches = cursor.matches(query, inline_tree.root_node(), content.as_bytes());
582 while let Some(m) = matches.next() {
583 for cap in m.captures {
584 let cname = names.get(cap.index as usize).copied().unwrap_or("");
585 if let Some(name) = recognized_highlight(cname) {
586 spans.push((cap.node.byte_range(), name));
587 }
588 }
589 }
590 spans.sort_by_key(|(r, _)| r.end - r.start);
591 spans
592 }
593}
594
595fn inline_node_ranges(tree: &Tree) -> Vec<tree_sitter::Range> {
598 let mut ranges = Vec::new();
599 let mut cursor = tree.walk();
600 loop {
601 let node = cursor.node();
602 if node.kind() == "inline" && node.end_byte() > node.start_byte() {
603 ranges.push(node.range());
604 }
605 if cursor.goto_first_child() {
606 continue;
607 }
608 loop {
609 if cursor.goto_next_sibling() {
610 break;
611 }
612 if !cursor.goto_parent() {
613 return ranges;
614 }
615 }
616 }
617}
618
619fn recognized_highlight(capture: &str) -> Option<&'static str> {
622 HIGHLIGHT_NAMES
623 .iter()
624 .copied()
625 .filter(|name| {
626 capture == *name
627 || capture
628 .strip_prefix(name)
629 .is_some_and(|r| r.starts_with('.'))
630 })
631 .max_by_key(|name| name.len())
632}
633
634impl Default for LanguageRegistry {
635 fn default() -> Self {
636 Self::build()
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 #[test]
643 fn exactly_the_layout_languages_are_layout_significant() {
644 let flagged: Vec<_> = super::REGISTRY
645 .entries
646 .iter()
647 .filter(|entry| entry.layout_significant)
648 .map(|entry| entry.name)
649 .collect();
650 assert_eq!(flagged, ["python", "yaml", "make", "scala", "haskell"]);
651 }
652}