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/// [`grammars::install`](crate::grammars::install) or from a provider of your
94/// own.
95fn 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
107/// Add a language, or replace the entry of the same name. Leaks: see the module
108/// note.
109pub 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
117/// The language a fence tag names. Tags are the raw first word of the fence
118/// info string — `rust {.numberLines}`, `rust,foo`, `Rust` — so they are
119/// trimmed at the first space or comma and case-folded before lookup.
120pub 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
133/// The language `path` is written in.
134///
135/// A whole name beats an extension — `Dockerfile` is not a `.file` — and
136/// between extensions the longest match wins, so `.d.ts` would beat `.ts`
137/// rather than racing it.
138pub 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
162/// Every name the registry holds, painted or not.
163pub 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
170/// Every name this build can paint.
171pub 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/// Every language this crate can name: name, fence tags, then file names and
183/// extensions. None carries a grammar — that is a provider's job, and until one
184/// registers, every one of these resolves to [`Known::Named`].
185#[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];