use std::borrow::Cow; use std::path::Path; use std::sync::LazyLock; pub(super) struct LanguageSpec { pub id: &'static str, pub name: &'static str, pub language: tree_sitter::Language, pub highlights: &'static str, pub injections: &'static str, pub locals: &'static str, } // These grammars extend the upstream base queries, rather than replacing them. static JS_HIGHLIGHTS: LazyLock = LazyLock::new(|| { [ tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_javascript::JSX_HIGHLIGHT_QUERY, ] .join("\n") }); static TS_HIGHLIGHTS: LazyLock = LazyLock::new(|| { [ tree_sitter_javascript::HIGHLIGHT_QUERY, tree_sitter_typescript::HIGHLIGHTS_QUERY, ] .join("\n") }); static TSX_HIGHLIGHTS: LazyLock = LazyLock::new(|| { [ JS_HIGHLIGHTS.as_str(), tree_sitter_typescript::HIGHLIGHTS_QUERY, ] .join("\n") }); static TS_LOCALS: LazyLock = LazyLock::new(|| { [ tree_sitter_javascript::LOCALS_QUERY, tree_sitter_typescript::LOCALS_QUERY, ] .join("\n") }); static CPP_HIGHLIGHTS: LazyLock = LazyLock::new(|| { [ tree_sitter_c::HIGHLIGHT_QUERY, tree_sitter_cpp::HIGHLIGHT_QUERY, ] .join("\n") }); static MD_HIGHLIGHTS: LazyLock = LazyLock::new(|| { [ tree_sitter_md::HIGHLIGHT_QUERY_BLOCK, " [ (atx_h1_marker) (atx_h2_marker) (atx_h3_marker) (atx_h4_marker) (atx_h5_marker) (atx_h6_marker) (setext_h1_underline) (setext_h2_underline) ] @text.title [ (list_marker_minus) (list_marker_plus) (list_marker_star) (list_marker_dot) (list_marker_parenthesis) ] @markup.list ", ] .join("\n") }); // Upstream's Lua patterns are regex-compatible; spell-check metadata is not a color. static ZIG_HIGHLIGHTS: LazyLock = LazyLock::new(|| { tree_sitter_zig::HIGHLIGHTS_QUERY .replace("#lua-match?", "#match?") .replace(" @spell", "") }); // The MIT-licensed upstream C++ crate bundles, but does not export, injections.scm. // This minimal AST query uses its raw-string delimiter/content nodes: // https://github.com/tree-sitter/tree-sitter-cpp/blob/v0.23.4/queries/injections.scm const CPP_INJECTIONS: &str = " (raw_string_literal delimiter: (raw_string_delimiter) @injection.language (raw_string_content) @injection.content) "; fn javascript_injections() -> &'static str { // The trailing upstream hbs/@glimmer rule uses a nonstandard #offset! directive, // not injection.* captures. Glimmer is outside the selected language set; // retain all standard tagged-template, regex and JSDoc injection queries. let query = tree_sitter_javascript::INJECTIONS_QUERY; query .split_once("\n; Parse Ember/") .map_or(query, |(query, _)| query) } fn spec( id: &'static str, name: &'static str, language: tree_sitter::Language, highlights: &'static str, injections: &'static str, locals: &'static str, ) -> LanguageSpec { LanguageSpec { id, name, language, highlights, injections, locals, } } static LANGUAGES: LazyLock<[LanguageSpec; 20]> = LazyLock::new(|| { [ spec( super::RUST_ID, "Rust", tree_sitter_rust::LANGUAGE.into(), tree_sitter_rust::HIGHLIGHTS_QUERY, tree_sitter_rust::INJECTIONS_QUERY, "", ), spec( super::RUBY_ID, "Ruby", tree_sitter_ruby::LANGUAGE.into(), tree_sitter_ruby::HIGHLIGHTS_QUERY, "", tree_sitter_ruby::LOCALS_QUERY, ), spec( super::PYTHON_ID, "Python", tree_sitter_python::LANGUAGE.into(), tree_sitter_python::HIGHLIGHTS_QUERY, "", "", ), spec( "json", "JSON", tree_sitter_json::LANGUAGE.into(), tree_sitter_json::HIGHLIGHTS_QUERY, "", "", ), spec( "shellscript", "Bash", tree_sitter_bash::LANGUAGE.into(), tree_sitter_bash::HIGHLIGHT_QUERY, "", "", ), spec( "makefile", "Makefile", tree_sitter_make::LANGUAGE.into(), tree_sitter_make::HIGHLIGHTS_QUERY, "", "", ), spec( "markdown", "Markdown", tree_sitter_md::LANGUAGE.into(), MD_HIGHLIGHTS.as_str(), tree_sitter_md::INJECTION_QUERY_BLOCK, "", ), spec( "javascript", "JavaScript", tree_sitter_javascript::LANGUAGE.into(), JS_HIGHLIGHTS.as_str(), javascript_injections(), tree_sitter_javascript::LOCALS_QUERY, ), spec( "typescript", "TypeScript", tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), TS_HIGHLIGHTS.as_str(), javascript_injections(), TS_LOCALS.as_str(), ), spec( "html", "HTML", tree_sitter_html::LANGUAGE.into(), tree_sitter_html::HIGHLIGHTS_QUERY, tree_sitter_html::INJECTIONS_QUERY, "", ), spec( "css", "CSS", tree_sitter_css::LANGUAGE.into(), tree_sitter_css::HIGHLIGHTS_QUERY, "", "", ), spec( "yaml", "YAML", tree_sitter_yaml::LANGUAGE.into(), tree_sitter_yaml::HIGHLIGHTS_QUERY, "", "", ), spec( "toml", "TOML", tree_sitter_toml_ng::LANGUAGE.into(), tree_sitter_toml_ng::HIGHLIGHTS_QUERY, "", "", ), spec( "c", "C", tree_sitter_c::LANGUAGE.into(), tree_sitter_c::HIGHLIGHT_QUERY, "", "", ), spec( "cpp", "C++", tree_sitter_cpp::LANGUAGE.into(), CPP_HIGHLIGHTS.as_str(), CPP_INJECTIONS, "", ), spec( "go", "Go", tree_sitter_go::LANGUAGE.into(), tree_sitter_go::HIGHLIGHTS_QUERY, "", "", ), spec( "typescriptreact", "TypeScript", tree_sitter_typescript::LANGUAGE_TSX.into(), TSX_HIGHLIGHTS.as_str(), javascript_injections(), TS_LOCALS.as_str(), ), spec( "markdown", "Markdown inline", tree_sitter_md::INLINE_LANGUAGE.into(), tree_sitter_md::HIGHLIGHT_QUERY_INLINE, tree_sitter_md::INJECTION_QUERY_INLINE, "", ), spec( "zig", "Zig", tree_sitter_zig::LANGUAGE.into(), ZIG_HIGHLIGHTS.as_str(), tree_sitter_zig::INJECTIONS_QUERY, "", ), spec( "honey", "Honey", tree_sitter_honey::LANGUAGE.into(), tree_sitter_honey::HIGHLIGHTS_QUERY, "", "", ), ] }); pub(super) fn all() -> impl Iterator { LANGUAGES.iter() } pub(super) fn by_name(name: &str) -> Option<&'static LanguageSpec> { let name = name.trim(); if name.eq_ignore_ascii_case("tsx") { return Some(&LANGUAGES[16]); } if name.eq_ignore_ascii_case("markdown_inline") || name.eq_ignore_ascii_case("markdown-inline") { return Some(&LANGUAGES[17]); } const ALIASES: &[(&str, &str)] = &[ ("rs", "Rust"), ("rb", "Ruby"), ("py", "Python"), ("sh", "Bash"), ("shell", "Bash"), ("shellscript", "Bash"), ("make", "Makefile"), ("mk", "Makefile"), ("md", "Markdown"), ("js", "JavaScript"), ("jsx", "JavaScript"), ("node", "JavaScript"), ("ecmascript", "JavaScript"), ("ts", "TypeScript"), ("htm", "HTML"), ("xhtml", "HTML"), ("yml", "YAML"), ("cc", "C++"), ("cpp", "C++"), ("cxx", "C++"), ("golang", "Go"), ("hon", "Honey"), ("honeylang", "Honey"), ]; let name = ALIASES .iter() .find(|(alias, _)| alias.eq_ignore_ascii_case(name)) .map_or(name, |(_, name)| *name); all().find(|spec| spec.name.eq_ignore_ascii_case(name)) } pub(super) fn detect( path: Option<&Path>, first_line: Option<&str>, ) -> Option<&'static LanguageSpec> { path.and_then(detect_path) .or_else(|| first_line.and_then(detect_shebang)) } fn detect_path(path: &Path) -> Option<&'static LanguageSpec> { let filename = path.file_name()?.to_str()?; let language = match filename { "Makefile" | "makefile" | "GNUmakefile" | "BSDmakefile" => "Makefile", "Gemfile" | "Rakefile" | "Guardfile" | "Vagrantfile" | "Brewfile" | "Podfile" | "Fastfile" | "Appfile" | "Berksfile" | ".irbrc" => "Ruby", "SConstruct" | "SConscript" => "Python", ".bashrc" | ".bash_profile" | ".bash_login" | ".bash_logout" | ".profile" => "Bash", "Cargo.lock" | "Pipfile" | "poetry.lock" | "uv.lock" => "TOML", _ => { let extension = path.extension()?.to_str()?; if matches!(extension, "C" | "H") { return by_name("C++"); } let extension = if extension.bytes().any(|byte| byte.is_ascii_uppercase()) { Cow::Owned(extension.to_ascii_lowercase()) } else { Cow::Borrowed(extension) }; match extension.as_ref() { "rs" => "Rust", "rb" | "rake" | "gemspec" => "Ruby", "py" | "pyw" | "pyi" => "Python", "json" => "JSON", "sh" | "bash" => "Bash", "mk" | "mak" | "make" => "Makefile", "md" | "markdown" | "mdown" | "mkd" | "mdwn" => "Markdown", "js" | "jsx" | "mjs" | "cjs" => "JavaScript", "ts" | "mts" | "cts" => "TypeScript", "tsx" => "tsx", "html" | "htm" | "xhtml" => "HTML", "css" => "CSS", "yaml" | "yml" => "YAML", "toml" => "TOML", "c" | "h" => "C", "cc" | "cpp" | "cxx" | "c++" | "hh" | "hpp" | "hxx" | "h++" | "ipp" | "inl" | "tpp" => "C++", "go" => "Go", "zig" => "Zig", "hon" => "Honey", _ => return None, } } }; by_name(language) } fn detect_shebang(line: &str) -> Option<&'static LanguageSpec> { let line = line .strip_prefix('\u{feff}') .unwrap_or(line) .strip_prefix("#!")?; let mut words = line.split_whitespace(); let mut program = words.next()?; if Path::new(program).file_name()?.to_str()? == "env" { program = loop { let word = words.next()?; if matches!(word, "-u" | "--unset" | "-C" | "--chdir" | "--argv0") { words.next()?; } else if !word.starts_with('-') && !word.contains('=') { break word; } }; } let program = Path::new(program).file_name()?.to_str()?; for (prefix, language) in [("python", "Python"), ("ruby", "Ruby")] { if let Some(suffix) = program.strip_prefix(prefix) && (suffix.is_empty() || (suffix.starts_with(|c: char| c.is_ascii_digit()) && suffix.chars().all(|c| c.is_ascii_digit() || c == '.'))) { return by_name(language); } } match program { "sh" | "bash" => by_name("Bash"), "node" | "nodejs" => by_name("JavaScript"), _ => None, } } #[cfg(test)] #[path = "../../tests/unit/syntax_languages.rs"] mod tests;