423 lines
12 KiB
Rust
423 lines
12 KiB
Rust
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<String> = LazyLock::new(|| {
|
|
[
|
|
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
|
tree_sitter_javascript::JSX_HIGHLIGHT_QUERY,
|
|
]
|
|
.join("\n")
|
|
});
|
|
static TS_HIGHLIGHTS: LazyLock<String> = LazyLock::new(|| {
|
|
[
|
|
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
|
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
|
]
|
|
.join("\n")
|
|
});
|
|
static TSX_HIGHLIGHTS: LazyLock<String> = LazyLock::new(|| {
|
|
[
|
|
JS_HIGHLIGHTS.as_str(),
|
|
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
|
]
|
|
.join("\n")
|
|
});
|
|
static TS_LOCALS: LazyLock<String> = LazyLock::new(|| {
|
|
[
|
|
tree_sitter_javascript::LOCALS_QUERY,
|
|
tree_sitter_typescript::LOCALS_QUERY,
|
|
]
|
|
.join("\n")
|
|
});
|
|
static CPP_HIGHLIGHTS: LazyLock<String> = LazyLock::new(|| {
|
|
[
|
|
tree_sitter_c::HIGHLIGHT_QUERY,
|
|
tree_sitter_cpp::HIGHLIGHT_QUERY,
|
|
]
|
|
.join("\n")
|
|
});
|
|
static MD_HIGHLIGHTS: LazyLock<String> = 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<String> = 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<Item = &'static LanguageSpec> {
|
|
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;
|