Skip to main content

syntax/
registry.rs

1//! Which languages this process can name, and which of those it can paint.
2//!
3//! [`LANGS`] seeds the registry; [`register`] adds to it at runtime. A name with
4//! no grammar is still an entry — [`Known::Named`] — so a caller can tell a
5//! language this build cannot paint from a file extension nobody has written a
6//! grammar for.
7//!
8//! Registrations are leaked. A language lives for the process, and the strings
9//! are a few hundred bytes against grammar bytes that are refcounted.
10
11use crate::lang::Lang;
12use std::{
13    fmt,
14    path::Path,
15    sync::{OnceLock, RwLock},
16};
17
18/// What the registry knows about a name.
19#[derive(Clone, Copy)]
20pub enum Known {
21    /// A grammar and a query are here.
22    Ready(&'static Lang),
23    /// Named, with nothing in this build to paint it.
24    Named(&'static str),
25}
26
27/// `Ready` compares by identity: entries hold `&'static Lang`, and two rows
28/// with the same name are the same row.
29impl 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
66/// One language the registry knows of.
67pub struct Entry {
68    pub name: &'static str,
69    /// Fence tags this answers to, lowercase.
70    pub aliases: &'static [&'static str],
71    /// Whole file names and extensions — `Dockerfile` is a name, `rs` an
72    /// extension. [`of_path`] tries whole names first.
73    pub files: &'static [&'static str],
74    /// `None` names a language this build cannot paint.
75    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
92/// The names alone. Grammars arrive through [`register`], from
93/// `bezel-syntax-std` or from a provider of your own.
94fn 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
106/// Add a language, or replace the entry of the same name. Leaks: see the module
107/// note.
108pub 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
116/// The language a fence tag names. Tags are the raw first word of the fence
117/// info string — `rust {.numberLines}`, `rust,foo`, `Rust` — so they are
118/// trimmed at the first space or comma and case-folded before lookup.
119pub 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
132/// The language `path` is written in.
133///
134/// A whole name beats an extension — `Dockerfile` is not a `.file` — and
135/// between extensions the longest match wins, so `.d.ts` would beat `.ts`
136/// rather than racing it.
137pub 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
161/// Every name the registry holds, painted or not.
162pub 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
169/// Every name this build can paint.
170pub 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/// Every language this crate can name: name, fence tags, then file names and
182/// extensions. None carries a grammar — that is a provider's job, and until one
183/// registers, every one of these resolves to [`Known::Named`].
184#[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];