1use crate::lang::Lang;
12use std::{
13 fmt,
14 path::Path,
15 sync::{OnceLock, RwLock},
16};
17
18#[derive(Clone, Copy)]
20pub enum Known {
21 Ready(&'static Lang),
23 Named(&'static str),
25}
26
27impl PartialEq for Known {
30 fn eq(&self, other: &Self) -> bool {
31 match (self, other) {
32 (Self::Ready(held), Self::Ready(other)) => std::ptr::eq(*held, *other),
33 (Self::Named(held), Self::Named(other)) => held == other,
34 _ => false,
35 }
36 }
37}
38
39impl Eq for Known {}
40
41impl fmt::Debug for Known {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::Ready(lang) => write!(f, "Ready({})", lang.name),
45 Self::Named(name) => write!(f, "Named({name})"),
46 }
47 }
48}
49
50impl Known {
51 pub fn name(self) -> &'static str {
52 match self {
53 Self::Ready(lang) => lang.name,
54 Self::Named(name) => name,
55 }
56 }
57
58 pub fn lang(self) -> Option<&'static Lang> {
59 match self {
60 Self::Ready(lang) => Some(lang),
61 Self::Named(_) => None,
62 }
63 }
64}
65
66pub struct Entry {
68 pub name: &'static str,
69 pub aliases: &'static [&'static str],
71 pub files: &'static [&'static str],
74 pub lang: Option<&'static Lang>,
76}
77
78impl Entry {
79 fn known(&self) -> Known {
80 match self.lang {
81 Some(lang) => Known::Ready(lang),
82 None => Known::Named(self.name),
83 }
84 }
85}
86
87fn registry() -> &'static RwLock<Vec<Entry>> {
88 static REGISTRY: OnceLock<RwLock<Vec<Entry>>> = OnceLock::new();
89 REGISTRY.get_or_init(|| RwLock::new(seeded()))
90}
91
92fn seeded() -> Vec<Entry> {
96 NAMED
97 .iter()
98 .map(|(name, aliases, files)| Entry {
99 name,
100 aliases,
101 files,
102 lang: None,
103 })
104 .collect()
105}
106
107pub fn register(entry: Entry) {
110 let Ok(mut entries) = registry().write() else {
111 return;
112 };
113 entries.retain(|held| held.name != entry.name);
114 entries.push(entry);
115}
116
117pub fn of_tag(tag: &str) -> Option<Known> {
121 let tag = tag
122 .split([' ', ','])
123 .next()
124 .unwrap_or("")
125 .to_ascii_lowercase();
126 let entries = registry().read().ok()?;
127 entries
128 .iter()
129 .find(|entry| entry.aliases.contains(&tag.as_str()))
130 .map(Entry::known)
131}
132
133pub fn of_path(path: &Path) -> Option<Known> {
139 let name = path.file_name()?.to_str()?;
140 let entries = registry().read().ok()?;
141 entries
142 .iter()
143 .filter_map(|entry| {
144 entry
145 .files
146 .iter()
147 .filter(|candidate| matches(name, candidate))
148 .map(|candidate| (candidate.len(), entry))
149 .max_by_key(|(len, _)| *len)
150 })
151 .max_by_key(|(len, _)| *len)
152 .map(|(_, entry)| entry.known())
153}
154
155fn matches(name: &str, candidate: &str) -> bool {
156 name.eq_ignore_ascii_case(candidate)
157 || name.len() > candidate.len()
158 && name[name.len() - candidate.len() - 1..]
159 .eq_ignore_ascii_case(&format!(".{candidate}"))
160}
161
162pub fn names() -> Vec<&'static str> {
164 let Ok(entries) = registry().read() else {
165 return Vec::new();
166 };
167 entries.iter().map(|entry| entry.name).collect()
168}
169
170pub fn ready() -> Vec<&'static str> {
172 let Ok(entries) = registry().read() else {
173 return Vec::new();
174 };
175 entries
176 .iter()
177 .filter(|entry| entry.lang.is_some())
178 .map(|entry| entry.name)
179 .collect()
180}
181
182#[rustfmt::skip]
186const NAMED: &[(&str, &[&str], &[&str])] = &[
187 ("bash", &["bash", "sh", "shell", "zsh", "console"], &[".bash_profile", ".bashrc", ".profile", ".zshrc", "bash", "sh", "zsh"]),
188 ("c", &["c"], &["c", "h"]),
189 ("cpp", &["cpp", "c++"], &["cc", "cpp", "cxx", "hpp"]),
190 ("csharp", &["csharp", "cs"], &["cs"]),
191 ("css", &["css", "scss"], &["css", "scss"]),
192 ("dockerfile", &["dockerfile"], &["Containerfile", "Dockerfile"]),
193 ("elixir", &["elixir", "ex"], &["ex", "exs"]),
194 ("go", &["go", "golang"], &["go"]),
195 ("graphql", &["graphql", "gql"], &["gql", "graphql"]),
196 ("haskell", &["haskell", "hs"], &["hs"]),
197 ("html", &["html"], &["htm", "html"]),
198 ("java", &["java"], &["java"]),
199 ("json", &["json", "jsonc"], &["json", "jsonc"]),
200 ("kotlin", &["kotlin", "kt"], &["kt", "kts"]),
201 ("lua", &["lua"], &["lua"]),
202 ("make", &["make", "makefile"], &["Makefile", "mk"]),
203 ("markdown", &["markdown", "md"], &["markdown", "md"]),
204 ("nix", &["nix"], &["nix"]),
205 ("php", &["php"], &["php"]),
206 ("proto", &["proto", "protobuf"], &["proto"]),
207 ("python", &["python", "py"], &["py", "pyi"]),
208 ("ruby", &["ruby", "rb"], &["Gemfile", "Rakefile", "erb", "rb"]),
209 ("rust", &["rust", "rs"], &["rs"]),
210 ("scala", &["scala"], &["sbt", "scala"]),
211 ("sql", &["sql"], &["sql"]),
212 ("svelte", &["svelte"], &["svelte"]),
213 ("swift", &["swift"], &["swift"]),
214 ("toml", &["toml"], &["toml"]),
215 ("tsx", &["tsx", "jsx", "javascript", "js"], &["cjs", "jsx", "mjs", "tsx"]),
216 ("typescript", &["typescript", "ts"], &["cts", "mts", "ts"]),
217 ("vue", &["vue"], &["vue"]),
218 ("xml", &["xml"], &["xml"]),
219 ("yaml", &["yaml", "yml"], &["yaml", "yml"]),
220 ("zig", &["zig"], &["zig"]),
221];