use crate::lang::Lang;
use std::{
fmt,
path::Path,
sync::{OnceLock, RwLock},
};
#[derive(Clone, Copy)]
pub enum Known {
Ready(&'static Lang),
Named(&'static str),
}
impl PartialEq for Known {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Ready(held), Self::Ready(other)) => std::ptr::eq(*held, *other),
(Self::Named(held), Self::Named(other)) => held == other,
_ => false,
}
}
}
impl Eq for Known {}
impl fmt::Debug for Known {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ready(lang) => write!(f, "Ready({})", lang.name),
Self::Named(name) => write!(f, "Named({name})"),
}
}
}
impl Known {
pub fn name(self) -> &'static str {
match self {
Self::Ready(lang) => lang.name,
Self::Named(name) => name,
}
}
pub fn lang(self) -> Option<&'static Lang> {
match self {
Self::Ready(lang) => Some(lang),
Self::Named(_) => None,
}
}
}
pub struct Entry {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub files: &'static [&'static str],
pub lang: Option<&'static Lang>,
}
impl Entry {
fn known(&self) -> Known {
match self.lang {
Some(lang) => Known::Ready(lang),
None => Known::Named(self.name),
}
}
}
fn registry() -> &'static RwLock<Vec<Entry>> {
static REGISTRY: OnceLock<RwLock<Vec<Entry>>> = OnceLock::new();
REGISTRY.get_or_init(|| RwLock::new(seeded()))
}
fn seeded() -> Vec<Entry> {
NAMED
.iter()
.map(|(name, aliases, files)| Entry {
name,
aliases,
files,
lang: None,
})
.collect()
}
pub fn register(entry: Entry) {
let Ok(mut entries) = registry().write() else {
return;
};
entries.retain(|held| held.name != entry.name);
entries.push(entry);
}
pub fn of_tag(tag: &str) -> Option<Known> {
let tag = tag
.split([' ', ','])
.next()
.unwrap_or("")
.to_ascii_lowercase();
let entries = registry().read().ok()?;
entries
.iter()
.find(|entry| entry.aliases.contains(&tag.as_str()))
.map(Entry::known)
}
pub fn of_path(path: &Path) -> Option<Known> {
let name = path.file_name()?.to_str()?;
let entries = registry().read().ok()?;
entries
.iter()
.filter_map(|entry| {
entry
.files
.iter()
.filter(|candidate| matches(name, candidate))
.map(|candidate| (candidate.len(), entry))
.max_by_key(|(len, _)| *len)
})
.max_by_key(|(len, _)| *len)
.map(|(_, entry)| entry.known())
}
fn matches(name: &str, candidate: &str) -> bool {
name.eq_ignore_ascii_case(candidate)
|| name.len() > candidate.len()
&& name[name.len() - candidate.len() - 1..]
.eq_ignore_ascii_case(&format!(".{candidate}"))
}
pub fn names() -> Vec<&'static str> {
let Ok(entries) = registry().read() else {
return Vec::new();
};
entries.iter().map(|entry| entry.name).collect()
}
pub fn ready() -> Vec<&'static str> {
let Ok(entries) = registry().read() else {
return Vec::new();
};
entries
.iter()
.filter(|entry| entry.lang.is_some())
.map(|entry| entry.name)
.collect()
}
#[rustfmt::skip]
const NAMED: &[(&str, &[&str], &[&str])] = &[
("bash", &["bash", "sh", "shell", "zsh", "console"], &[".bash_profile", ".bashrc", ".profile", ".zshrc", "bash", "sh", "zsh"]),
("c", &["c"], &["c", "h"]),
("cpp", &["cpp", "c++"], &["cc", "cpp", "cxx", "hpp"]),
("csharp", &["csharp", "cs"], &["cs"]),
("css", &["css", "scss"], &["css", "scss"]),
("dockerfile", &["dockerfile"], &["Containerfile", "Dockerfile"]),
("elixir", &["elixir", "ex"], &["ex", "exs"]),
("go", &["go", "golang"], &["go"]),
("graphql", &["graphql", "gql"], &["gql", "graphql"]),
("haskell", &["haskell", "hs"], &["hs"]),
("html", &["html"], &["htm", "html"]),
("java", &["java"], &["java"]),
("json", &["json", "jsonc"], &["json", "jsonc"]),
("kotlin", &["kotlin", "kt"], &["kt", "kts"]),
("lua", &["lua"], &["lua"]),
("make", &["make", "makefile"], &["Makefile", "mk"]),
("markdown", &["markdown", "md"], &["markdown", "md"]),
("nix", &["nix"], &["nix"]),
("php", &["php"], &["php"]),
("proto", &["proto", "protobuf"], &["proto"]),
("python", &["python", "py"], &["py", "pyi"]),
("ruby", &["ruby", "rb"], &["Gemfile", "Rakefile", "erb", "rb"]),
("rust", &["rust", "rs"], &["rs"]),
("scala", &["scala"], &["sbt", "scala"]),
("sql", &["sql"], &["sql"]),
("svelte", &["svelte"], &["svelte"]),
("swift", &["swift"], &["swift"]),
("toml", &["toml"], &["toml"]),
("tsx", &["tsx", "jsx", "javascript", "js"], &["cjs", "jsx", "mjs", "tsx"]),
("typescript", &["typescript", "ts"], &["cts", "mts", "ts"]),
("vue", &["vue"], &["vue"]),
("xml", &["xml"], &["xml"]),
("yaml", &["yaml", "yml"], &["yaml", "yml"]),
("zig", &["zig"], &["zig"]),
];