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> {
95 NAMED
96 .iter()
97 .map(|(name, aliases, files)| Entry {
98 name,
99 aliases,
100 files,
101 lang: None,
102 })
103 .collect()
104}
105
106pub fn register(entry: Entry) {
109 let Ok(mut entries) = registry().write() else {
110 return;
111 };
112 entries.retain(|held| held.name != entry.name);
113 entries.push(entry);
114}
115
116pub fn of_tag(tag: &str) -> Option<Known> {
120 let tag = tag
121 .split([' ', ','])
122 .next()
123 .unwrap_or("")
124 .to_ascii_lowercase();
125 let entries = registry().read().ok()?;
126 entries
127 .iter()
128 .find(|entry| entry.aliases.contains(&tag.as_str()))
129 .map(Entry::known)
130}
131
132pub fn of_path(path: &Path) -> Option<Known> {
138 let name = path.file_name()?.to_str()?;
139 let entries = registry().read().ok()?;
140 entries
141 .iter()
142 .filter_map(|entry| {
143 entry
144 .files
145 .iter()
146 .filter(|candidate| matches(name, candidate))
147 .map(|candidate| (candidate.len(), entry))
148 .max_by_key(|(len, _)| *len)
149 })
150 .max_by_key(|(len, _)| *len)
151 .map(|(_, entry)| entry.known())
152}
153
154fn matches(name: &str, candidate: &str) -> bool {
155 name.eq_ignore_ascii_case(candidate)
156 || name.len() > candidate.len()
157 && name[name.len() - candidate.len() - 1..]
158 .eq_ignore_ascii_case(&format!(".{candidate}"))
159}
160
161pub fn names() -> Vec<&'static str> {
163 let Ok(entries) = registry().read() else {
164 return Vec::new();
165 };
166 entries.iter().map(|entry| entry.name).collect()
167}
168
169pub fn ready() -> Vec<&'static str> {
171 let Ok(entries) = registry().read() else {
172 return Vec::new();
173 };
174 entries
175 .iter()
176 .filter(|entry| entry.lang.is_some())
177 .map(|entry| entry.name)
178 .collect()
179}
180
181#[rustfmt::skip]
185const NAMED: &[(&str, &[&str], &[&str])] = &[
186 ("bash", &["bash", "sh", "shell", "zsh", "console"], &[".bash_profile", ".bashrc", ".profile", ".zshrc", "bash", "sh", "zsh"]),
187 ("c", &["c"], &["c", "h"]),
188 ("cpp", &["cpp", "c++"], &["cc", "cpp", "cxx", "hpp"]),
189 ("csharp", &["csharp", "cs"], &["cs"]),
190 ("css", &["css", "scss"], &["css", "scss"]),
191 ("dockerfile", &["dockerfile"], &["Containerfile", "Dockerfile"]),
192 ("elixir", &["elixir", "ex"], &["ex", "exs"]),
193 ("go", &["go", "golang"], &["go"]),
194 ("graphql", &["graphql", "gql"], &["gql", "graphql"]),
195 ("haskell", &["haskell", "hs"], &["hs"]),
196 ("html", &["html"], &["htm", "html"]),
197 ("java", &["java"], &["java"]),
198 ("json", &["json", "jsonc"], &["json", "jsonc"]),
199 ("kotlin", &["kotlin", "kt"], &["kt", "kts"]),
200 ("lua", &["lua"], &["lua"]),
201 ("make", &["make", "makefile"], &["Makefile", "mk"]),
202 ("markdown", &["markdown", "md"], &["markdown", "md"]),
203 ("nix", &["nix"], &["nix"]),
204 ("php", &["php"], &["php"]),
205 ("proto", &["proto", "protobuf"], &["proto"]),
206 ("python", &["python", "py"], &["py", "pyi"]),
207 ("ruby", &["ruby", "rb"], &["Gemfile", "Rakefile", "erb", "rb"]),
208 ("rust", &["rust", "rs"], &["rs"]),
209 ("scala", &["scala"], &["sbt", "scala"]),
210 ("sql", &["sql"], &["sql"]),
211 ("svelte", &["svelte"], &["svelte"]),
212 ("swift", &["swift"], &["swift"]),
213 ("toml", &["toml"], &["toml"]),
214 ("tsx", &["tsx", "jsx", "javascript", "js"], &["cjs", "jsx", "mjs", "tsx"]),
215 ("typescript", &["typescript", "ts"], &["cts", "mts", "ts"]),
216 ("vue", &["vue"], &["vue"]),
217 ("xml", &["xml"], &["xml"]),
218 ("yaml", &["yaml", "yml"], &["yaml", "yml"]),
219 ("zig", &["zig"], &["zig"]),
220];