1use std::collections::{HashMap, HashSet};
16use std::sync::{Arc, LazyLock};
17use unicase::UniCase;
18
19pub struct TreeSitterLanguageInfo {
21 pub tree_sitter_lang: tree_sitter::Language,
22 pub terminal_node_kind_ids: HashSet<u16>,
23}
24
25impl TreeSitterLanguageInfo {
26 #[allow(dead_code)]
27 fn new(
28 lang_fn: impl Into<tree_sitter::Language>,
29 terminal_node_kinds: impl IntoIterator<Item = &'static str>,
30 ) -> Self {
31 let tree_sitter_lang: tree_sitter::Language = lang_fn.into();
32 let terminal_node_kind_ids = terminal_node_kinds
33 .into_iter()
34 .filter_map(|kind| {
35 let id = tree_sitter_lang.id_for_node_kind(kind, true);
36 if id != 0 {
37 Some(id)
38 } else {
39 None
41 }
42 })
43 .collect();
44 Self {
45 tree_sitter_lang,
46 terminal_node_kind_ids,
47 }
48 }
49}
50
51pub struct ProgrammingLanguageInfo {
53 pub name: Arc<str>,
57
58 pub treesitter_info: Option<TreeSitterLanguageInfo>,
60}
61
62static LANGUAGE_INFO_BY_NAME: LazyLock<
63 HashMap<UniCase<&'static str>, Arc<ProgrammingLanguageInfo>>,
64> = LazyLock::new(|| {
65 let mut map = HashMap::new();
66
67 let mut add = |name: &'static str,
71 aliases: &[&'static str],
72 treesitter_info: Option<TreeSitterLanguageInfo>| {
73 let config = Arc::new(ProgrammingLanguageInfo {
74 name: Arc::from(name),
75 treesitter_info,
76 });
77 for name in std::iter::once(name).chain(aliases.iter().copied()) {
78 if map.insert(name.into(), config.clone()).is_some() {
79 panic!("Language `{name}` already exists");
80 }
81 }
82 };
83
84 add("actionscript", &[".as"], None);
86 add("ada", &[".ada", ".adb", ".ads"], None);
87 add("agda", &[".agda"], None);
88 add("apex", &[".cls", ".trigger"], None);
89 add("arduino", &[".ino"], None);
90 add("asm", &[".asm", ".a51", ".i", ".nas", ".nasm", ".s"], None);
91 add("astro", &[".astro"], None);
92 add("bash", &[".sh", ".bash"], None);
93 add("beancount", &[".beancount"], None);
94 add("bibtex", &[".bib", ".bibtex"], None);
95 add("bicep", &[".bicep", ".bicepparam"], None);
96 add("bitbake", &[".bb", ".bbappend", ".bbclass"], None);
97 cfg_if::cfg_if! {
98 if #[cfg(feature = "c")] {
99 add(
100 "c",
101 &[".c", ".cats", ".h.in", ".idc"],
102 Some(TreeSitterLanguageInfo::new(tree_sitter_c::LANGUAGE, [])),
103 );
104 } else {
105 add("c", &[".c", ".cats", ".h.in", ".idc"], None);
106 }
107 }
108 add("cairo", &[".cairo"], None);
109 add("capnp", &[".capnp"], None);
110 add("chatito", &[".chatito"], None);
111 add("clarity", &[".clar"], None);
112 add(
113 "clojure",
114 &[
115 ".clj", ".boot", ".cl2", ".cljc", ".cljs", ".cljs.hl", ".cljscm", ".cljx", ".hic",
116 ],
117 None,
118 );
119 add("cmake", &[".cmake", ".cmake.in"], None);
120 add(
121 "commonlisp",
122 &[
123 ".lisp", ".asd", ".cl", ".l", ".lsp", ".ny", ".podsl", ".sexp",
124 ],
125 None,
126 );
127 cfg_if::cfg_if! {
128 if #[cfg(feature = "cpp")] {
129 add(
130 "cpp",
131 &[
132 ".cpp", ".h", ".c++", ".cc", ".cp", ".cppm", ".cxx", ".h++", ".hh", ".hpp", ".hxx",
133 ".inl", ".ipp", ".ixx", ".tcc", ".tpp", ".txx", "c++",
134 ],
135 Some(TreeSitterLanguageInfo::new(tree_sitter_cpp::LANGUAGE, [])),
136 );
137 } else {
138 add(
139 "cpp",
140 &[
141 ".cpp", ".h", ".c++", ".cc", ".cp", ".cppm", ".cxx", ".h++", ".hh", ".hpp", ".hxx",
142 ".inl", ".ipp", ".ixx", ".tcc", ".tpp", ".txx", "c++",
143 ],
144 None,
145 );
146 }
147 }
148 add("cpon", &[".cpon"], None);
149 cfg_if::cfg_if! {
150 if #[cfg(feature = "c-sharp")] {
151 add(
152 "csharp",
153 &[".cs", ".cake", ".cs.pp", ".csx", ".linq", "cs", "c#"],
154 Some(TreeSitterLanguageInfo::new(
155 tree_sitter_c_sharp::LANGUAGE,
156 [],
157 )),
158 );
159 } else {
160 add(
161 "csharp",
162 &[".cs", ".cake", ".cs.pp", ".csx", ".linq", "cs", "c#"],
163 None,
164 );
165 }
166 }
167 cfg_if::cfg_if! {
168 if #[cfg(feature = "css")] {
169 add(
170 "css",
171 &[".css", ".scss"],
172 Some(TreeSitterLanguageInfo::new(tree_sitter_css::LANGUAGE, [])),
173 );
174 } else {
175 add("css", &[".css", ".scss"], None);
176 }
177 }
178 add("csv", &[".csv"], None);
179 add("cuda", &[".cu", ".cuh"], None);
180 add("d", &[".d", ".di"], None);
181 add("dart", &[".dart"], None);
182 add("dockerfile", &[".dockerfile", ".containerfile"], None);
183 cfg_if::cfg_if! {
184 if #[cfg(feature = "xml")] {
185 add(
186 "dtd",
187 &[".dtd"],
188 Some(TreeSitterLanguageInfo::new(
189 tree_sitter_xml::LANGUAGE_DTD,
190 [],
191 )),
192 );
193 } else {
194 add("dtd", &[".dtd"], None);
195 }
196 }
197 add("elisp", &[".el"], None);
198 add("elixir", &[".ex", ".exs"], None);
199 add("elm", &[".elm"], None);
200 add("embeddedtemplate", &[".ets"], None);
201 add(
202 "erlang",
203 &[
204 ".erl", ".app", ".app.src", ".escript", ".hrl", ".xrl", ".yrl",
205 ],
206 None,
207 );
208 add("fennel", &[".fnl"], None);
209 add("firrtl", &[".fir"], None);
210 add("fish", &[".fish"], None);
211 cfg_if::cfg_if! {
212 if #[cfg(feature = "fortran")] {
213 add(
214 "fortran",
215 &[".f", ".f90", ".f95", ".f03", "f", "f90", "f95", "f03"],
216 Some(TreeSitterLanguageInfo::new(
217 tree_sitter_fortran::LANGUAGE,
218 [],
219 )),
220 );
221 } else {
222 add(
223 "fortran",
224 &[".f", ".f90", ".f95", ".f03", "f", "f90", "f95", "f03"],
225 None,
226 );
227 }
228 }
229 add("fsharp", &[".fs", ".fsi", ".fsx"], None);
230 add("func", &[".func"], None);
231 add("gdscript", &[".gd"], None);
232 add("gitattributes", &[".gitattributes"], None);
233 add("gitignore", &[".gitignore"], None);
234 add("gleam", &[".gleam"], None);
235 add("glsl", &[".glsl", ".vert", ".frag"], None);
236 add("gn", &[".gn", ".gni"], None);
237 cfg_if::cfg_if! {
238 if #[cfg(feature = "go")] {
239 add(
240 "go",
241 &[".go", "golang"],
242 Some(TreeSitterLanguageInfo::new(tree_sitter_go::LANGUAGE, [])),
243 );
244 } else {
245 add("go", &[".go", "golang"], None);
246 }
247 }
248 add("gomod", &["go.mod"], None);
249 add("gosum", &["go.sum"], None);
250 add("graphql", &[".graphql", ".gql"], None);
251 add(
252 "groovy",
253 &[".groovy", ".grt", ".gtpl", ".gvy", ".gradle"],
254 None,
255 );
256 add("hack", &[".hack"], None);
257 add("hare", &[".ha"], None);
258 add("haskell", &[".hs", ".hs-boot", ".hsc"], None);
259 add("haxe", &[".hx"], None);
260 add("hcl", &[".hcl", ".tf"], None);
261 add("heex", &[".heex"], None);
262 add("hlsl", &[".hlsl"], None);
263 cfg_if::cfg_if! {
264 if #[cfg(feature = "html")] {
265 add(
266 "html",
267 &[".html", ".htm", ".hta", ".html.hl", ".xht", ".xhtml"],
268 Some(TreeSitterLanguageInfo::new(tree_sitter_html::LANGUAGE, [])),
269 );
270 } else {
271 add("html", &[".html", ".htm", ".hta", ".html.hl", ".xht", ".xhtml"], None);
272 }
273 }
274 add("hyprlang", &[".hl"], None);
275 add("ini", &[".ini", ".cfg"], None);
276 add("ispc", &[".ispc"], None);
277 add("janet", &[".janet"], None);
278 cfg_if::cfg_if! {
279 if #[cfg(feature = "java")] {
280 add(
281 "java",
282 &[".java", ".jav", ".jsh"],
283 Some(TreeSitterLanguageInfo::new(tree_sitter_java::LANGUAGE, [])),
284 );
285 } else {
286 add("java", &[".java", ".jav", ".jsh"], None);
287 }
288 }
289 cfg_if::cfg_if! {
290 if #[cfg(feature = "javascript")] {
291 add(
292 "javascript",
293 &[
294 ".js",
295 "._js",
296 ".bones",
297 ".cjs",
298 ".es",
299 ".es6",
300 ".gs",
301 ".jake",
302 ".javascript",
303 ".jsb",
304 ".jscad",
305 ".jsfl",
306 ".jslib",
307 ".jsm",
308 ".jspre",
309 ".jss",
310 ".jsx",
311 ".mjs",
312 ".njs",
313 ".pac",
314 ".sjs",
315 ".ssjs",
316 ".xsjs",
317 ".xsjslib",
318 "js",
319 ],
320 Some(TreeSitterLanguageInfo::new(
321 tree_sitter_javascript::LANGUAGE,
322 [],
323 )),
324 );
325 } else {
326 add("javascript", &[
327 ".js",
328 "._js",
329 ".bones",
330 ".cjs",
331 ".es",
332 ".es6",
333 ".gs",
334 ".jake",
335 ".javascript",
336 ".jsb",
337 ".jscad",
338 ".jsfl",
339 ".jslib",
340 ".jsm",
341 ".jspre",
342 ".jss",
343 ".jsx",
344 ".mjs",
345 ".njs",
346 ".pac",
347 ".sjs",
348 ".ssjs",
349 ".xsjs",
350 ".xsjslib",
351 "js",
352 ], None);
353 }
354 }
355 cfg_if::cfg_if! {
356 if #[cfg(feature = "json")] {
357 add(
358 "json",
359 &[
360 ".json",
361 ".4DForm",
362 ".4DProject",
363 ".avsc",
364 ".geojson",
365 ".gltf",
366 ".har",
367 ".ice",
368 ".JSON-tmLanguage",
369 ".json.example",
370 ".jsonl",
371 ".mcmeta",
372 ".sarif",
373 ".tact",
374 ".tfstate",
375 ".tfstate.backup",
376 ".topojson",
377 ".webapp",
378 ".webmanifest",
379 ".yy",
380 ".yyp",
381 ],
382 Some(TreeSitterLanguageInfo::new(tree_sitter_json::LANGUAGE, [])),
383 );
384 } else {
385 add("json", &[
386 ".json",
387 ".4DForm",
388 ".4DProject",
389 ".avsc",
390 ".geojson",
391 ".gltf",
392 ".har",
393 ".ice",
394 ".JSON-tmLanguage",
395 ".json.example",
396 ".jsonl",
397 ".mcmeta",
398 ".sarif",
399 ".tact",
400 ".tfstate",
401 ".tfstate.backup",
402 ".topojson",
403 ".webapp",
404 ".webmanifest",
405 ".yy",
406 ".yyp",
407 ], None);
408 }
409 }
410 add("jsonnet", &[".jsonnet"], None);
411 add("julia", &[".jl"], None);
412 add("kdl", &[".kdl"], None);
413 cfg_if::cfg_if! {
414 if #[cfg(feature = "kotlin")] {
415 add(
416 "kotlin",
417 &[".kt", ".ktm", ".kts"],
418 Some(TreeSitterLanguageInfo::new(
419 tree_sitter_kotlin_ng::LANGUAGE,
420 [],
421 )),
422 );
423 } else {
424 add("kotlin", &[".kt", ".ktm", ".kts"], None);
425 }
426 }
427 add("latex", &[".tex"], None);
428 add("linkerscript", &[".ld"], None);
429 add("llvm", &[".ll"], None);
430 add(
431 "lua",
432 &[
433 ".lua",
434 ".nse",
435 ".p8",
436 ".pd_lua",
437 ".rbxs",
438 ".rockspec",
439 ".wlua",
440 ],
441 None,
442 );
443 add("luau", &[".luau"], None);
444 add("magik", &[".magik"], None);
445 add(
446 "make",
447 &[".mak", ".make", ".makefile", ".mk", ".mkfile"],
448 None,
449 );
450 cfg_if::cfg_if! {
451 if #[cfg(feature = "markdown")] {
452 add(
453 "markdown",
454 &[
455 ".md",
456 ".livemd",
457 ".markdown",
458 ".mdown",
459 ".mdwn",
460 ".mdx",
461 ".mkd",
462 ".mkdn",
463 ".mkdown",
464 ".ronn",
465 ".scd",
466 ".workbook",
467 "md",
468 ],
469 Some(TreeSitterLanguageInfo::new(
470 tree_sitter_md::LANGUAGE,
471 ["inline", "indented_code_block", "fenced_code_block"],
472 )),
473 );
474 } else {
475 add("markdown", &[".md", ".livemd", ".markdown", ".mdown", ".mdwn", ".mdx", ".mkd", ".mkdn", ".mkdown", ".ronn", ".scd", ".workbook", "md"], None);
476 }
477 }
478 add("mermaid", &[".mmd"], None);
479 add("meson", &["meson.build"], None);
480 add("netlinx", &[".axi"], None);
481 add(
482 "nim",
483 &[".nim", ".nim.cfg", ".nimble", ".nimrod", ".nims"],
484 None,
485 );
486 add("ninja", &[".ninja"], None);
487 add("nix", &[".nix"], None);
488 add("nqc", &[".nqc"], None);
489 cfg_if::cfg_if! {
490 if #[cfg(feature = "pascal")] {
491 add(
492 "pascal",
493 &[
494 ".pas", ".dfm", ".dpr", ".lpr", ".pascal", "pas", "dpr", "delphi",
495 ],
496 Some(TreeSitterLanguageInfo::new(
497 tree_sitter_pascal::LANGUAGE,
498 [],
499 )),
500 );
501 } else {
502 add("pascal", &[".pas", ".dfm", ".dpr", ".lpr", ".pascal", "pas", "dpr", "delphi"], None);
503 }
504 }
505 add("pem", &[".pem"], None);
506 add(
507 "perl",
508 &[
509 ".pl", ".al", ".cgi", ".fcgi", ".perl", ".ph", ".plx", ".pm", ".psgi", ".t",
510 ],
511 None,
512 );
513 add("pgn", &[".pgn"], None);
514 cfg_if::cfg_if! {
515 if #[cfg(feature = "php")] {
516 add(
517 "php",
518 &[".php"],
519 Some(TreeSitterLanguageInfo::new(
520 tree_sitter_php::LANGUAGE_PHP,
521 [],
522 )),
523 );
524 } else {
525 add("php", &[".php"], None);
526 }
527 }
528 add("po", &[".po"], None);
529 add("pony", &[".pony"], None);
530 add("powershell", &[".ps1"], None);
531 add("prisma", &[".prisma"], None);
532 add("properties", &[".properties"], None);
533 add("proto", &[".proto"], None);
534 add("psv", &[".psv"], None);
535 add("puppet", &[".pp"], None);
536 add("purescript", &[".purs"], None);
537 cfg_if::cfg_if! {
538 if #[cfg(feature = "python")] {
539 add(
540 "python",
541 &[".py", ".pyw", ".pyi", ".pyx", ".pxd", ".pxi"],
542 Some(TreeSitterLanguageInfo::new(
543 tree_sitter_python::LANGUAGE,
544 [],
545 )),
546 );
547 } else {
548 add("python", &[".py", ".pyw", ".pyi", ".pyx", ".pxd", ".pxi"], None);
549 }
550 }
551 add("qmljs", &[".qml"], None);
552 cfg_if::cfg_if! {
553 if #[cfg(feature = "r")] {
554 add(
555 "r",
556 &[".r"],
557 Some(TreeSitterLanguageInfo::new(tree_sitter_r::LANGUAGE, [])),
558 );
559 } else {
560 add("r", &[".r"], None);
561 }
562 }
563 add("racket", &[".rkt"], None);
564 add("rbs", &[".rbs"], None);
565 add("re2c", &[".re"], None);
566 add("rego", &[".rego"], None);
567 add("requirements", &["requirements.txt"], None);
568 add("ron", &[".ron"], None);
569 add("rst", &[".rst"], None);
570 cfg_if::cfg_if! {
571 if #[cfg(feature = "ruby")] {
572 add(
573 "ruby",
574 &[".rb"],
575 Some(TreeSitterLanguageInfo::new(tree_sitter_ruby::LANGUAGE, [])),
576 );
577 } else {
578 add("ruby", &[".rb"], None);
579 }
580 }
581 cfg_if::cfg_if! {
582 if #[cfg(feature = "rust")] {
583 add(
584 "rust",
585 &[".rs", "rs"],
586 Some(TreeSitterLanguageInfo::new(tree_sitter_rust::LANGUAGE, [])),
587 );
588 } else {
589 add("rust", &[".rs", "rs"], None);
590 }
591 }
592 cfg_if::cfg_if! {
593 if #[cfg(feature = "scala")] {
594 add(
595 "scala",
596 &[".scala"],
597 Some(TreeSitterLanguageInfo::new(tree_sitter_scala::LANGUAGE, [])),
598 );
599 } else {
600 add("scala", &[".scala"], None);
601 }
602 }
603 add("scheme", &[".ss"], None);
604 add("slang", &[".slang"], None);
605 add("smali", &[".smali"], None);
606 add("smithy", &[".smithy"], None);
607 cfg_if::cfg_if! {
608 if #[cfg(feature = "solidity")] {
609 add(
610 "solidity",
611 &[".sol"],
612 Some(TreeSitterLanguageInfo::new(
613 tree_sitter_solidity::LANGUAGE,
614 [],
615 )),
616 );
617 } else {
618 add("solidity", &[".sol"], None);
619 }
620 }
621 add("sparql", &[".sparql"], None);
622 cfg_if::cfg_if! {
623 if #[cfg(feature = "sql")] {
624 add(
625 "sql",
626 &[".sql"],
627 Some(TreeSitterLanguageInfo::new(
628 tree_sitter_sequel::LANGUAGE,
629 [],
630 )),
631 );
632 } else {
633 add("sql", &[".sql"], None);
634 }
635 }
636 add("squirrel", &[".nut"], None);
637 add("starlark", &[".star", ".bzl"], None);
638 add("svelte", &[".svelte"], None);
639 cfg_if::cfg_if! {
640 if #[cfg(feature = "swift")] {
641 add(
642 "swift",
643 &[".swift"],
644 Some(TreeSitterLanguageInfo::new(tree_sitter_swift::LANGUAGE, [])),
645 );
646 } else {
647 add("swift", &[".swift"], None);
648 }
649 }
650 add("tablegen", &[".td"], None);
651 add("tcl", &[".tcl"], None);
652 add("thrift", &[".thrift"], None);
653 cfg_if::cfg_if! {
654 if #[cfg(feature = "toml")] {
655 add(
656 "toml",
657 &[".toml"],
658 Some(TreeSitterLanguageInfo::new(
659 tree_sitter_toml_ng::LANGUAGE,
660 [],
661 )),
662 );
663 } else {
664 add("toml", &[".toml"], None);
665 }
666 }
667 add("tsv", &[".tsv"], None);
668 cfg_if::cfg_if! {
669 if #[cfg(feature = "typescript")] {
670 add(
671 "tsx",
672 &[".tsx"],
673 Some(TreeSitterLanguageInfo::new(
674 tree_sitter_typescript::LANGUAGE_TSX,
675 [],
676 )),
677 );
678 } else {
679 add("tsx", &[".tsx"], None);
680 }
681 }
682 add("twig", &[".twig"], None);
683 cfg_if::cfg_if! {
684 if #[cfg(feature = "typescript")] {
685 add(
686 "typescript",
687 &[".ts", "ts"],
688 Some(TreeSitterLanguageInfo::new(
689 tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
690 [],
691 )),
692 );
693 } else {
694 add("typescript", &[".ts", "ts"], None);
695 }
696 }
697 add("typst", &[".typ"], None);
698 add("udev", &[".rules"], None);
699 add("ungrammar", &[".ungram"], None);
700 add("uxntal", &[".tal"], None);
701 add("verilog", &[".vh"], None);
702 add("vhdl", &[".vhd", ".vhdl"], None);
703 add("vim", &[".vim"], None);
704 add("vue", &[".vue"], None);
705 add("wast", &[".wast"], None);
706 add("wat", &[".wat"], None);
707 add("wgsl", &[".wgsl"], None);
708 add("xcompose", &[".xcompose"], None);
709 cfg_if::cfg_if! {
710 if #[cfg(feature = "xml")] {
711 add(
712 "xml",
713 &[".xml"],
714 Some(TreeSitterLanguageInfo::new(
715 tree_sitter_xml::LANGUAGE_XML,
716 [],
717 )),
718 );
719 } else {
720 add("xml", &[".xml"], None);
721 }
722 }
723 cfg_if::cfg_if! {
724 if #[cfg(feature = "yaml")] {
725 add(
726 "yaml",
727 &[".yaml", ".yml"],
728 Some(TreeSitterLanguageInfo::new(tree_sitter_yaml::LANGUAGE, [])),
729 );
730 } else {
731 add("yaml", &[".yaml", ".yml"], None);
732 }
733 }
734 add("yuck", &[".yuck"], None);
735 add("zig", &[".zig"], None);
736
737 map
738});
739
740pub fn get_language_info(name: &str) -> Option<&ProgrammingLanguageInfo> {
745 LANGUAGE_INFO_BY_NAME
746 .get(&UniCase::new(name))
747 .map(|info| info.as_ref())
748}
749
750pub fn detect_language(filename: &str) -> Option<&str> {
754 let last_dot = filename.rfind('.')?;
755 let extension = &filename[last_dot..];
756 get_language_info(extension).map(|info| info.name.as_ref())
757}
758
759#[cfg(test)]
760mod tests {
761 use super::*;
762
763 #[cfg(any(
764 feature = "all",
765 feature = "c",
766 feature = "c-sharp",
767 feature = "cpp",
768 feature = "css",
769 feature = "fortran",
770 feature = "go",
771 feature = "html",
772 feature = "java",
773 feature = "javascript",
774 feature = "json",
775 feature = "kotlin",
776 feature = "markdown",
777 feature = "pascal",
778 feature = "php",
779 feature = "python",
780 feature = "r",
781 feature = "ruby",
782 feature = "rust",
783 feature = "scala",
784 feature = "solidity",
785 feature = "sql",
786 feature = "swift",
787 feature = "toml",
788 feature = "typescript",
789 feature = "xml",
790 feature = "yaml"
791 ))]
792 #[test]
793 fn test_get_language_info() {
794 let rust_info = get_language_info(".rs").unwrap();
795 assert_eq!(rust_info.name.as_ref(), "rust");
796 assert!(rust_info.treesitter_info.is_some());
797
798 let py_info = get_language_info(".py").unwrap();
799 assert_eq!(py_info.name.as_ref(), "python");
800
801 let rust_upper = get_language_info(".RS").unwrap();
803 assert_eq!(rust_upper.name.as_ref(), "rust");
804
805 assert!(get_language_info(".unknown").is_none());
807 }
808
809 #[test]
810 fn test_detect_language() {
811 assert_eq!(detect_language("test.rs"), Some("rust"));
812 assert_eq!(detect_language("main.py"), Some("python"));
813 assert_eq!(detect_language("app.js"), Some("javascript"));
814 assert_eq!(detect_language("noextension"), None);
815 assert_eq!(detect_language("unknown.xyz"), None);
816 }
817}