use anyhow::{Context, Result};
use serde::{Serialize, Serializer};
use std::path::Path;
use std::sync::OnceLock;
use strum_macros::{Display, EnumString, FromRepr};
use syntect::parsing::{SyntaxReference, SyntaxSet};
#[repr(u16)]
#[derive(Clone, Copy, Debug, Display, EnumString, Eq, FromRepr, PartialEq)]
#[strum(serialize_all = "kebab-case", ascii_case_insensitive)]
pub(crate) enum Language {
Assembly = 0,
C = 1,
Bash = 2,
Cpp = 3,
CSharp = 4,
Css = 5,
Dockerfile = 6,
Go = 7,
Gdscript = 8,
Java = 9,
JavaScript = 10,
Jsx = 11,
Html = 12,
Json = 13,
Kconfig = 14,
Latex = 15,
Lua = 16,
Markdown = 17,
Xml = 18,
Yaml = 19,
Just = 20,
Make = 21,
Meson = 22,
Nix = 23,
Perl = 24,
Python = 25,
Php = 26,
Puppet = 27,
Ruby = 28,
Riscv = 29,
Rust = 30,
Swift = 31,
Sql = 32,
TypeScript = 33,
Typst = 34,
Toml = 35,
Tsx = 36,
Vimscript = 37,
Zig = 38,
Unknown = 39,
}
impl From<Language> for u16 {
fn from(language: Language) -> Self {
language as u16
}
}
impl Language {
pub(crate) fn id(self) -> &'static str {
language_spec(self).map_or("unknown", |spec| spec.id)
}
}
#[derive(Clone, Copy)]
pub(crate) struct LanguageSpec {
pub(crate) language: Language,
pub(crate) id: &'static str,
pub(crate) engine: Option<AnalysisEngine>,
pub(crate) aliases: &'static [&'static str],
pub(crate) extensions: &'static [&'static str],
pub(crate) file_names: &'static [&'static str],
pub(crate) syntax_names: &'static [&'static str],
pub(crate) has_symbols: bool,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, FromRepr, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub(crate) enum AnalysisEngine {
TreeSitter = 0,
Llvm = 1,
}
impl From<AnalysisEngine> for u8 {
fn from(engine: AnalysisEngine) -> Self {
engine as u8
}
}
#[allow(clippy::ref_option, clippy::trivially_copy_pass_by_ref)]
pub(crate) fn serialize_engine<S: Serializer>(
engine: &Option<AnalysisEngine>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match engine {
Some(e) => e.serialize(serializer),
None => serializer.serialize_str("none"),
}
}
pub(crate) const LANGUAGE_SPECS: &[LanguageSpec] = &[
LanguageSpec {
language: Language::Assembly,
id: "assembly",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["assembly", "asm", "x86", "arm"],
extensions: &["asm", "s", "S"],
file_names: &[],
syntax_names: &["Assembly", "ARM Assembly", "x86 Assembly"],
has_symbols: false,
},
LanguageSpec {
language: Language::C,
id: "c",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["c"],
extensions: &["c", "h"],
file_names: &[],
syntax_names: &["C"],
has_symbols: true,
},
LanguageSpec {
language: Language::Bash,
id: "bash",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["bash", "sh", "shell"],
extensions: &["bash", "sh"],
file_names: &[".bashrc", ".bash_profile", ".profile"],
syntax_names: &[
"Bourne Again Shell (bash)",
"Shell-Unix-Generic",
"ShellScript",
],
has_symbols: true,
},
LanguageSpec {
language: Language::Cpp,
id: "cpp",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["cpp", "cxx", "cplusplus"],
extensions: &["cc", "cpp", "cxx", "hh", "hpp", "hxx"],
file_names: &[],
syntax_names: &["C++"],
has_symbols: true,
},
LanguageSpec {
language: Language::CSharp,
id: "csharp",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["csharp", "cs", "c#"],
extensions: &["cs"],
file_names: &[],
syntax_names: &["C#"],
has_symbols: true,
},
LanguageSpec {
language: Language::Css,
id: "css",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["css"],
extensions: &["css"],
file_names: &[],
syntax_names: &["CSS"],
has_symbols: false,
},
LanguageSpec {
language: Language::Dockerfile,
id: "dockerfile",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["dockerfile", "containerfile"],
extensions: &[],
file_names: &["Dockerfile", "Containerfile"],
syntax_names: &["Dockerfile"],
has_symbols: false,
},
LanguageSpec {
language: Language::Go,
id: "go",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["go", "golang"],
extensions: &["go"],
file_names: &["go.mod"],
syntax_names: &["Go"],
has_symbols: true,
},
LanguageSpec {
language: Language::Gdscript,
id: "gdscript",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["gdscript", "gd"],
extensions: &["gd"],
file_names: &[],
syntax_names: &["GDScript"],
has_symbols: false,
},
LanguageSpec {
language: Language::Java,
id: "java",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["java"],
extensions: &["java"],
file_names: &[],
syntax_names: &["Java"],
has_symbols: true,
},
LanguageSpec {
language: Language::JavaScript,
id: "javascript",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["javascript", "js"],
extensions: &["js", "mjs", "cjs"],
file_names: &[],
syntax_names: &["JavaScript"],
has_symbols: true,
},
LanguageSpec {
language: Language::Jsx,
id: "jsx",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["jsx"],
extensions: &["jsx"],
file_names: &[],
syntax_names: &[],
has_symbols: true,
},
LanguageSpec {
language: Language::Html,
id: "html",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["html", "htm"],
extensions: &["html", "htm"],
file_names: &[],
syntax_names: &["HTML"],
has_symbols: false,
},
LanguageSpec {
language: Language::Json,
id: "json",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["json"],
extensions: &["json"],
file_names: &["package-lock.json", "composer.lock"],
syntax_names: &["JSON"],
has_symbols: false,
},
LanguageSpec {
language: Language::Xml,
id: "xml",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["xml"],
extensions: &["xml", "xsd", "xsl", "xslt"],
file_names: &[],
syntax_names: &["XML"],
has_symbols: false,
},
LanguageSpec {
language: Language::Yaml,
id: "yaml",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["yaml", "yml"],
extensions: &["yaml", "yml"],
file_names: &[],
syntax_names: &["YAML"],
has_symbols: false,
},
LanguageSpec {
language: Language::Just,
id: "just",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["just", "justfile"],
extensions: &["just"],
file_names: &["justfile", "Justfile", ".justfile"],
syntax_names: &["Just"],
has_symbols: true,
},
LanguageSpec {
language: Language::Kconfig,
id: "kconfig",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["kconfig"],
extensions: &[],
file_names: &["Kconfig"],
syntax_names: &["Kconfig"],
has_symbols: true,
},
LanguageSpec {
language: Language::Make,
id: "make",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["make", "makefile"],
extensions: &["mk", "mak", "make"],
file_names: &["Makefile", "makefile", "GNUmakefile"],
syntax_names: &["Makefile"],
has_symbols: true,
},
LanguageSpec {
language: Language::Latex,
id: "latex",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["latex", "tex"],
extensions: &["tex", "ltx", "latex"],
file_names: &[],
syntax_names: &["LaTeX", "TeX"],
has_symbols: false,
},
LanguageSpec {
language: Language::Lua,
id: "lua",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["lua"],
extensions: &["lua"],
file_names: &[],
syntax_names: &["Lua"],
has_symbols: false,
},
LanguageSpec {
language: Language::Markdown,
id: "markdown",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["markdown", "md"],
extensions: &["md", "markdown", "mdown", "mkd"],
file_names: &[],
syntax_names: &["Markdown"],
has_symbols: true,
},
LanguageSpec {
language: Language::Meson,
id: "meson",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["meson"],
extensions: &[],
file_names: &["meson.build", "meson_options.txt"],
syntax_names: &["Meson"],
has_symbols: false,
},
LanguageSpec {
language: Language::Nix,
id: "nix",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["nix"],
extensions: &["nix"],
file_names: &["flake.lock"],
syntax_names: &["Nix"],
has_symbols: false,
},
LanguageSpec {
language: Language::Perl,
id: "perl",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["perl", "pl", "pm"],
extensions: &["pl", "pm", "t"],
file_names: &[],
syntax_names: &["Perl"],
has_symbols: false,
},
LanguageSpec {
language: Language::Python,
id: "python",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["python", "py"],
extensions: &["py", "pyw"],
file_names: &[],
syntax_names: &["Python"],
has_symbols: true,
},
LanguageSpec {
language: Language::Php,
id: "php",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["php"],
extensions: &["php", "php3", "php4", "php5", "phtml"],
file_names: &[],
syntax_names: &["PHP"],
has_symbols: true,
},
LanguageSpec {
language: Language::Puppet,
id: "puppet",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["puppet", "pp"],
extensions: &["pp"],
file_names: &["Puppetfile"],
syntax_names: &["Puppet"],
has_symbols: false,
},
LanguageSpec {
language: Language::Ruby,
id: "ruby",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["ruby", "rb"],
extensions: &["rb", "rake", "gemspec"],
file_names: &["Gemfile", "Rakefile"],
syntax_names: &["Ruby"],
has_symbols: true,
},
LanguageSpec {
language: Language::Riscv,
id: "riscv",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["riscv", "risc-v", "riscv64"],
extensions: &["riscv"],
file_names: &[],
syntax_names: &["RISC-V"],
has_symbols: false,
},
LanguageSpec {
language: Language::Rust,
id: "rust",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["rust", "rs"],
extensions: &["rs"],
file_names: &[],
syntax_names: &["Rust"],
has_symbols: true,
},
LanguageSpec {
language: Language::Swift,
id: "swift",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["swift"],
extensions: &["swift"],
file_names: &[],
syntax_names: &["Swift"],
has_symbols: true,
},
LanguageSpec {
language: Language::Sql,
id: "sql",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["sql"],
extensions: &["sql"],
file_names: &[],
syntax_names: &["SQL"],
has_symbols: false,
},
LanguageSpec {
language: Language::TypeScript,
id: "typescript",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["typescript", "ts"],
extensions: &["ts", "mts", "cts"],
file_names: &[],
syntax_names: &["TypeScript"],
has_symbols: true,
},
LanguageSpec {
language: Language::Tsx,
id: "tsx",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["tsx"],
extensions: &["tsx"],
file_names: &[],
syntax_names: &[],
has_symbols: true,
},
LanguageSpec {
language: Language::Toml,
id: "toml",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["toml"],
extensions: &["toml"],
file_names: &["Cargo.lock"],
syntax_names: &["TOML"],
has_symbols: false,
},
LanguageSpec {
language: Language::Typst,
id: "typst",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["typst", "typ"],
extensions: &["typ"],
file_names: &[],
syntax_names: &["Typst"],
has_symbols: false,
},
LanguageSpec {
language: Language::Vimscript,
id: "vimscript",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["vimscript", "vim", "viml"],
extensions: &["vim", "vimrc", "gvimrc"],
file_names: &[".vimrc", ".gvimrc"],
syntax_names: &["VimL", "Vimscript"],
has_symbols: true,
},
LanguageSpec {
language: Language::Zig,
id: "zig",
engine: Some(AnalysisEngine::TreeSitter),
aliases: &["zig"],
extensions: &["zig", "zon"],
file_names: &["build.zig", "build.zig.zon"],
syntax_names: &["Zig"],
has_symbols: false,
},
];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum BinaryMode {
Reject,
Detect,
Lossy,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum DocumentKind {
Source,
Text,
}
impl Serialize for Language {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.id())
}
}
pub(crate) fn detect_language(path: &Path, text: &str) -> (Language, Option<String>) {
if let Some(language) = detect_by_path(path) {
return (language, None);
}
if let Some(line) = text.lines().next()
&& line.starts_with("#!")
{
if line.contains("python") {
return (Language::Python, None);
}
if line.contains("node") {
return (Language::JavaScript, None);
}
}
let syntax_set = {
static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines)
};
let syntax = detect_syntax(syntax_set, path, text);
let Some(syntax) = syntax else {
return (Language::Unknown, None);
};
(
LANGUAGE_SPECS
.iter()
.find_map(|spec| {
spec.syntax_names
.contains(&syntax.name.as_str())
.then_some(spec.language)
})
.unwrap_or(Language::Unknown),
Some(syntax.name.clone()),
)
}
fn detect_syntax<'a>(
syntax_set: &'a SyntaxSet,
path: &Path,
text: &str,
) -> Option<&'a SyntaxReference> {
if let Some(name) = path.file_name().and_then(|name| name.to_str())
&& let Some(syntax) = syntax_set.find_syntax_by_extension(name)
{
return Some(syntax);
}
if let Some(ext) = path.extension().and_then(|ext| ext.to_str())
&& let Some(syntax) = syntax_set.find_syntax_by_extension(ext)
{
return Some(syntax);
}
let first_line = text.lines().next().unwrap_or("");
syntax_set.find_syntax_by_first_line(first_line)
}
pub(crate) fn detect_by_path(path: &Path) -> Option<Language> {
let file_name = path.file_name()?.to_str()?;
LANGUAGE_SPECS
.iter()
.find_map(|spec| {
spec.file_names
.contains(&file_name)
.then_some(spec.language)
})
.or_else(|| {
let extension = path.extension()?.to_str()?;
LANGUAGE_SPECS.iter().find_map(|spec| {
spec.extensions
.iter()
.any(|&ext| ext.eq_ignore_ascii_case(extension))
.then_some(spec.language)
})
})
}
pub(crate) fn language_spec(language: Language) -> Option<&'static LanguageSpec> {
LANGUAGE_SPECS.iter().find(|spec| spec.language == language)
}
pub(crate) fn extract_plain_text(
path: &Path,
bytes: Vec<u8>,
binary_mode: BinaryMode,
) -> Result<String> {
match binary_mode {
BinaryMode::Lossy => match String::from_utf8(bytes) {
Ok(text) => Ok(text),
Err(error) => Ok(String::from_utf8_lossy(error.as_bytes()).into_owned()),
},
BinaryMode::Reject | BinaryMode::Detect => String::from_utf8(bytes)
.with_context(|| format!("{} is not UTF-8 text", path.display())),
}
}
pub(crate) fn normalize_source_text(mut text: String) -> String {
if text.starts_with('\u{feff}') {
text.drain(..'\u{feff}'.len_utf8());
}
if !text.contains('\r') {
return text;
}
text.replace("\r\n", "\n").replace('\r', "\n")
}