Skip to main content

recoco_splitters/
prog_langs.rs

1// Recoco is a Rust-only fork of CocoIndex, by [CocoIndex](https://CocoIndex)
2// Original code from CocoIndex is copyrighted by CocoIndex
3// SPDX-FileCopyrightText: 2025-2026 CocoIndex (upstream)
4// SPDX-FileContributor: CocoIndex Contributors
5//
6// All modifications from the upstream for Recoco are copyrighted by Knitli Inc.
7// SPDX-FileCopyrightText: 2026 Knitli Inc. (Recoco)
8// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
9//
10// Both the upstream CocoIndex code and the Recoco modifications are licensed under the Apache-2.0 License.
11// SPDX-License-Identifier: Apache-2.0
12
13//! Programming language detection and tree-sitter support.
14
15use std::collections::{HashMap, HashSet};
16use std::sync::{Arc, LazyLock};
17use unicase::UniCase;
18
19/// Tree-sitter language information for syntax-aware parsing.
20pub 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                    // Node kind not found - this is a configuration issue
40                    None
41                }
42            })
43            .collect();
44        Self {
45            tree_sitter_lang,
46            terminal_node_kind_ids,
47        }
48    }
49}
50
51/// Information about a programming language.
52pub struct ProgrammingLanguageInfo {
53    /// The main name of the language.
54    /// It's expected to be consistent with the language names listed at:
55    /// <https://github.com/Goldziher/tree-sitter-language-pack/tree/main?tab=readme-ov-file#available-languages>
56    pub name: Arc<str>,
57
58    /// Optional tree-sitter language info for syntax-aware parsing.
59    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    // Adds a language to the global map of languages.
68    // `name` is the main name of the language, used to set the `name` field of the `ProgrammingLanguageInfo`.
69    // `aliases` are the other names of the language, which can be language names or file extensions (e.g. `.js`, `.py`).
70    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    // Languages sorted alphabetically by name
85    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
740/// Get programming language info by name or file extension.
741///
742/// The lookup is case-insensitive and supports both language names
743/// (e.g., "rust", "python") and file extensions (e.g., ".rs", ".py").
744pub 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
750/// Detect programming language from a filename.
751///
752/// Returns the language name if the file extension is recognized.
753pub 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        // Case insensitive
802        let rust_upper = get_language_info(".RS").unwrap();
803        assert_eq!(rust_upper.name.as_ref(), "rust");
804
805        // Unknown extension
806        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}