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 Some(tree_sitter_typescript::TAGS_QUERY),
167 );
168 r.add(
169 "tsx",
170 &["tsx"],
171 tree_sitter_typescript::LANGUAGE_TSX.into(),
172 format!(
173 "{}\n{}\n{}",
174 tree_sitter_javascript::HIGHLIGHT_QUERY,
175 tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
176 tree_sitter_typescript::HIGHLIGHTS_QUERY
177 ),
178 Some(tree_sitter_typescript::TAGS_QUERY),
179 );
180 r.add(
181 "go",
182 &["go"],
183 tree_sitter_go::LANGUAGE.into(),
184 tree_sitter_go::HIGHLIGHTS_QUERY,
185 Some(tree_sitter_go::TAGS_QUERY),
186 );
187 r.add(
188 "c",
189 &["c", "h"],
190 tree_sitter_c::LANGUAGE.into(),
191 tree_sitter_c::HIGHLIGHT_QUERY,
192 Some(tree_sitter_c::TAGS_QUERY),
193 );
194 r.add(
195 "cpp",
196 &["cpp", "cc", "cxx", "hpp", "hh", "hxx"],
197 tree_sitter_cpp::LANGUAGE.into(),
198 format!(
200 "{}\n{}",
201 tree_sitter_c::HIGHLIGHT_QUERY,
202 tree_sitter_cpp::HIGHLIGHT_QUERY
203 ),
204 Some(tree_sitter_cpp::TAGS_QUERY),
205 );
206 r.add(
207 "java",
208 &["java"],
209 tree_sitter_java::LANGUAGE.into(),
210 tree_sitter_java::HIGHLIGHTS_QUERY,
211 Some(tree_sitter_java::TAGS_QUERY),
212 );
213 r.add(
214 "c-sharp",
215 &["cs"],
216 tree_sitter_c_sharp::LANGUAGE.into(),
217 tree_sitter_c_sharp::HIGHLIGHTS_QUERY,
218 Some(tree_sitter_c_sharp::TAGS_QUERY),
219 );
220 r.add(
221 "ruby",
222 &["rb"],
223 tree_sitter_ruby::LANGUAGE.into(),
224 tree_sitter_ruby::HIGHLIGHTS_QUERY,
225 Some(tree_sitter_ruby::TAGS_QUERY),
226 );
227 r.add(
228 "php",
229 &["php"],
230 tree_sitter_php::LANGUAGE_PHP.into(),
231 tree_sitter_php::HIGHLIGHTS_QUERY,
232 Some(tree_sitter_php::TAGS_QUERY),
233 );
234 r.add(
235 "bash",
236 &["sh", "bash", "zsh"],
237 tree_sitter_bash::LANGUAGE.into(),
238 tree_sitter_bash::HIGHLIGHT_QUERY,
239 None,
240 );
241 r.add(
242 "json",
243 &["json"],
244 tree_sitter_json::LANGUAGE.into(),
245 tree_sitter_json::HIGHLIGHTS_QUERY,
246 None,
247 );
248 r.register(
249 "html",
250 &["html", "htm"],
251 tree_sitter_html::LANGUAGE.into(),
252 tree_sitter_html::HIGHLIGHTS_QUERY,
253 tree_sitter_html::INJECTIONS_QUERY,
254 None,
255 );
256 r.add(
257 "css",
258 &["css"],
259 tree_sitter_css::LANGUAGE.into(),
260 tree_sitter_css::HIGHLIGHTS_QUERY,
261 None,
262 );
263 r.add(
264 "yaml",
265 &["yml", "yaml"],
266 tree_sitter_yaml::LANGUAGE.into(),
267 tree_sitter_yaml::HIGHLIGHTS_QUERY,
268 None,
269 );
270 r.layout_significant();
271 r.add(
276 "sql",
277 &["sql"],
278 tree_sitter_sequel::LANGUAGE.into(),
279 format!(
280 "{}\n((literal) @number (#match? @number \"^[-+]?[0-9][0-9.]*$\"))\n",
281 tree_sitter_sequel::HIGHLIGHTS_QUERY
282 ),
283 None,
284 );
285 r.register(
289 "markdown",
290 &["md", "markdown"],
291 tree_sitter_md::LANGUAGE.into(),
292 tree_sitter_md::HIGHLIGHT_QUERY_BLOCK,
293 tree_sitter_md::INJECTION_QUERY_BLOCK,
294 None,
295 );
296 let md_inline: Language = tree_sitter_md::INLINE_LANGUAGE.into();
297 r.markdown_inline_query =
298 Query::new(&md_inline, tree_sitter_md::HIGHLIGHT_QUERY_INLINE).ok();
299 r.register(
300 "markdown_inline",
301 &[],
302 md_inline,
303 tree_sitter_md::HIGHLIGHT_QUERY_INLINE,
304 "",
305 None,
306 );
307
308 r.add(
309 "toml",
310 &["toml"],
311 tree_sitter_toml_ng::LANGUAGE.into(),
312 tree_sitter_toml_ng::HIGHLIGHTS_QUERY,
313 None,
314 );
315 r.add(
317 "hcl",
318 &["tf", "tfvars", "hcl"],
319 tree_sitter_hcl::LANGUAGE.into(),
320 include_str!("../../queries/hcl/highlights.scm"),
321 None,
322 );
323 r.add(
324 "dockerfile",
325 &["dockerfile", "containerfile"],
326 arborium_dockerfile::language().into(),
327 arborium_dockerfile::HIGHLIGHTS_QUERY,
328 None,
329 );
330 r.add(
331 "make",
332 &["mk"],
333 tree_sitter_make::LANGUAGE.into(),
334 tree_sitter_make::HIGHLIGHTS_QUERY,
335 None,
336 );
337 r.layout_significant();
338 r.add(
339 "lua",
340 &["lua"],
341 tree_sitter_lua::LANGUAGE.into(),
342 tree_sitter_lua::HIGHLIGHTS_QUERY,
343 Some(tree_sitter_lua::TAGS_QUERY),
344 );
345 r.add(
346 "nix",
347 &["nix"],
348 tree_sitter_nix::LANGUAGE.into(),
349 tree_sitter_nix::HIGHLIGHTS_QUERY,
350 None,
351 );
352 r.add(
353 "xml",
354 &["xml", "xsd", "xslt", "svg"],
355 tree_sitter_xml::LANGUAGE_XML.into(),
356 tree_sitter_xml::XML_HIGHLIGHT_QUERY,
357 None,
358 );
359 r.add(
360 "swift",
361 &["swift"],
362 tree_sitter_swift::LANGUAGE.into(),
363 tree_sitter_swift::HIGHLIGHTS_QUERY,
364 Some(tree_sitter_swift::TAGS_QUERY),
365 );
366 r.add(
367 "scala",
368 &["scala", "sbt"],
369 tree_sitter_scala::LANGUAGE.into(),
370 tree_sitter_scala::HIGHLIGHTS_QUERY,
371 None,
372 );
373 r.layout_significant();
374 r.add(
375 "elixir",
376 &["ex", "exs"],
377 tree_sitter_elixir::LANGUAGE.into(),
378 tree_sitter_elixir::HIGHLIGHTS_QUERY,
379 Some(tree_sitter_elixir::TAGS_QUERY),
380 );
381 r.add(
382 "zig",
383 &["zig", "zon"],
384 tree_sitter_zig::LANGUAGE.into(),
385 tree_sitter_zig::HIGHLIGHTS_QUERY,
386 None,
387 );
388 r.add(
389 "haskell",
390 &["hs"],
391 tree_sitter_haskell::LANGUAGE.into(),
392 tree_sitter_haskell::HIGHLIGHTS_QUERY,
393 None,
394 );
395 r.layout_significant();
396 r.add(
397 "dart",
398 &["dart"],
399 tree_sitter_dart::LANGUAGE.into(),
400 tree_sitter_dart::HIGHLIGHTS_QUERY,
401 Some(tree_sitter_dart::TAGS_QUERY),
402 );
403 r.add(
404 "powershell",
405 &["ps1", "psm1", "psd1"],
406 tree_sitter_powershell::LANGUAGE.into(),
407 tree_sitter_powershell::HIGHLIGHTS_QUERY,
408 None,
409 );
410 r.register(
411 "svelte",
412 &["svelte"],
413 tree_sitter_svelte_ng::LANGUAGE.into(),
414 format!(
417 "{}\n{}",
418 tree_sitter_html::HIGHLIGHTS_QUERY,
419 tree_sitter_svelte_ng::HIGHLIGHTS_QUERY
420 ),
421 tree_sitter_svelte_ng::INJECTIONS_QUERY,
422 None,
423 );
424 r.name_files("make", &["makefile", "gnumakefile"]);
425 r.name_files("dockerfile", &["dockerfile", "containerfile"]);
426
427 r
428 }
429
430 fn name_files(&mut self, name: &'static str, filenames: &'static [&'static str]) {
432 let Some(&idx) = self.by_name.get(name) else {
433 return;
434 };
435 for file in filenames {
436 self.by_filename.insert(file, idx);
437 }
438 }
439
440 fn add(
441 &mut self,
442 name: &'static str,
443 extensions: &'static [&'static str],
444 language: Language,
445 highlights: impl Into<Cow<'static, str>>,
446 tags: Option<&'static str>,
447 ) {
448 self.register(name, extensions, language, highlights, "", tags);
449 }
450
451 fn register(
452 &mut self,
453 name: &'static str,
454 extensions: &'static [&'static str],
455 language: Language,
456 highlights: impl Into<Cow<'static, str>>,
457 injections: impl Into<Cow<'static, str>>,
458 tags: Option<&'static str>,
459 ) {
460 let idx = self.entries.len();
461 self.entries.push(LangEntry {
462 name,
463 language,
464 highlights: highlights.into(),
465 injections: injections.into(),
466 tags_query: tags.map(Cow::Borrowed),
467 layout_significant: false,
468 config: OnceLock::new(),
469 tags: OnceLock::new(),
470 });
471 self.by_name.insert(name, idx);
472 for ext in extensions {
473 self.by_ext.insert(ext, idx);
474 }
475 }
476
477 fn layout_significant(&mut self) {
481 if let Some(entry) = self.entries.last_mut() {
482 entry.layout_significant = true;
483 }
484 }
485
486 pub fn for_path(&self, path: &str) -> Option<&LangEntry> {
489 let name = Path::new(path).file_name()?.to_str()?.to_ascii_lowercase();
490 if let Some(&idx) = self.by_filename.get(name.as_str()) {
491 return self.entries.get(idx);
492 }
493 let ext = Path::new(path).extension()?.to_str()?;
494 let &idx = self.by_ext.get(ext)?;
495 self.entries.get(idx)
496 }
497
498 pub fn for_token(&self, token: &str) -> Option<&LangEntry> {
501 let token = token.trim().to_ascii_lowercase();
502 let token = match token.as_str() {
503 "c++" => "cpp",
504 "c#" | "csharp" => "cs",
505 "shell" => "bash",
506 "golang" => "go",
507 "terraform" => "hcl",
508 "docker" => "dockerfile",
509 "makefile" => "make",
510 "pwsh" | "ps" => "powershell",
511 other => other,
512 };
513 let &idx = self.by_name.get(token).or_else(|| self.by_ext.get(token))?;
514 self.entries.get(idx)
515 }
516
517 pub fn config_for_injection(&self, lang: &str) -> Option<&HighlightConfiguration> {
521 self.for_token(lang)?.config()
522 }
523
524 pub fn markdown_inline_spans(&self, content: &str) -> Vec<(Range<usize>, &'static str)> {
529 if content.len() > MAX_PARSE_BYTES {
530 return Vec::new();
531 }
532 let (Some(query), Some(block), Some(inline)) = (
533 self.markdown_inline_query.as_ref(),
534 self.by_name
535 .get("markdown")
536 .and_then(|&i| self.entries.get(i)),
537 self.by_name
538 .get("markdown_inline")
539 .and_then(|&i| self.entries.get(i)),
540 ) else {
541 return Vec::new();
542 };
543
544 let mut bp = Parser::new();
545 if bp.set_language(&block.language).is_err() {
546 return Vec::new();
547 }
548 let Some(block_tree) = bp.parse(content, None) else {
549 return Vec::new();
550 };
551 let ranges = inline_node_ranges(&block_tree);
552 if ranges.is_empty() {
553 return Vec::new();
554 }
555
556 let mut ip = Parser::new();
557 if ip.set_included_ranges(&ranges).is_err() || ip.set_language(&inline.language).is_err() {
558 return Vec::new();
559 }
560 let Some(inline_tree) = ip.parse(content, None) else {
561 return Vec::new();
562 };
563
564 let names = query.capture_names();
565 let mut cursor = QueryCursor::new();
566 let mut spans = Vec::new();
567 let mut matches = cursor.matches(query, inline_tree.root_node(), content.as_bytes());
568 while let Some(m) = matches.next() {
569 for cap in m.captures {
570 let cname = names.get(cap.index as usize).copied().unwrap_or("");
571 if let Some(name) = recognized_highlight(cname) {
572 spans.push((cap.node.byte_range(), name));
573 }
574 }
575 }
576 spans.sort_by_key(|(r, _)| r.end - r.start);
577 spans
578 }
579}
580
581fn inline_node_ranges(tree: &Tree) -> Vec<tree_sitter::Range> {
584 let mut ranges = Vec::new();
585 let mut cursor = tree.walk();
586 loop {
587 let node = cursor.node();
588 if node.kind() == "inline" && node.end_byte() > node.start_byte() {
589 ranges.push(node.range());
590 }
591 if cursor.goto_first_child() {
592 continue;
593 }
594 loop {
595 if cursor.goto_next_sibling() {
596 break;
597 }
598 if !cursor.goto_parent() {
599 return ranges;
600 }
601 }
602 }
603}
604
605fn recognized_highlight(capture: &str) -> Option<&'static str> {
608 HIGHLIGHT_NAMES
609 .iter()
610 .copied()
611 .filter(|name| {
612 capture == *name
613 || capture
614 .strip_prefix(name)
615 .is_some_and(|r| r.starts_with('.'))
616 })
617 .max_by_key(|name| name.len())
618}
619
620impl Default for LanguageRegistry {
621 fn default() -> Self {
622 Self::build()
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 #[test]
629 fn exactly_the_layout_languages_are_layout_significant() {
630 let flagged: Vec<_> = super::REGISTRY
631 .entries
632 .iter()
633 .filter(|entry| entry.layout_significant)
634 .map(|entry| entry.name)
635 .collect();
636 assert_eq!(flagged, ["python", "yaml", "make", "scala", "haskell"]);
637 }
638}