Skip to main content

syntax/
lang.rs

1//! The languages bezel can highlight: a row per grammar — the fence aliases
2//! it answers to, the tree-sitter grammar, and its highlights query. Each row
3//! is behind the feature of the same name.
4
5use std::{ops::Range, sync::OnceLock};
6use theme::HighlightKind;
7use tree_sitter::Language;
8use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter};
9use tree_sitter_language::LanguageFn;
10
11pub struct Lang {
12    pub name: &'static str,
13    pub aliases: &'static [&'static str],
14    pub grammar: LanguageFn,
15    pub query: &'static str,
16    /// Compiling a highlights query costs milliseconds — tsx.scm is 750 lines
17    /// — and a render loop calls [`Lang::compiled`] every frame.
18    compiled: OnceLock<Option<Compiled>>,
19}
20
21/// A grammar's query, compiled and configured once.
22pub struct Compiled {
23    pub config: HighlightConfiguration,
24    /// Capture names by `Highlight` index, for [`kind_of`].
25    pub names: Vec<String>,
26}
27
28impl Lang {
29    /// A language of your own: a grammar, its highlights query, and the fence
30    /// tags it answers to. `const`, so it can be a `static` beside the built-in
31    /// rows and reach [`Lang::highlight`] the same way they do.
32    pub const fn new(
33        name: &'static str,
34        aliases: &'static [&'static str],
35        grammar: LanguageFn,
36        query: &'static str,
37    ) -> Self {
38        Self {
39            name,
40            aliases,
41            grammar,
42            query,
43            compiled: OnceLock::new(),
44        }
45    }
46
47    /// Spans over `source`, in bytes, in document order. `None` when the query
48    /// does not compile against the grammar.
49    pub fn highlight(&'static self, source: &str) -> Option<Vec<(Range<usize>, HighlightKind)>> {
50        let compiled = self.compiled()?;
51        let config = &compiled.config;
52        let mut highlighter = Highlighter::new();
53        highlighter.parser().set_language(&config.language).ok()?;
54        let mut spans = Vec::new();
55        // Nested highlight starts end with `HighlightEnd`; the top of the stack
56        // is the kind painting the `Source` ranges that follow it.
57        let mut kinds: Vec<HighlightKind> = Vec::new();
58        for event in highlighter
59            .highlight(config, source.as_bytes(), None, |_| None)
60            .ok()?
61            .flatten()
62        {
63            match event {
64                HighlightEvent::HighlightStart(hl) => {
65                    let name = compiled.names.get(hl.0).map(String::as_str).unwrap_or("");
66                    kinds.push(kind_of(name));
67                }
68                HighlightEvent::HighlightEnd => {
69                    kinds.pop();
70                }
71                HighlightEvent::Source { start, end } => {
72                    if let Some(&kind) = kinds.last() {
73                        spans.push((start..end, kind));
74                    }
75                }
76            }
77        }
78        Some(spans)
79    }
80
81    pub fn compiled(&'static self) -> Option<&'static Compiled> {
82        self.compiled
83            .get_or_init(|| {
84                let grammar: Language = self.grammar.into();
85                let mut config =
86                    HighlightConfiguration::new(grammar, self.name, self.query, "", "").ok()?;
87                // Recognize exactly the capture names the query uses, so every
88                // `Highlight` index resolves straight through `names`.
89                // `_`-prefixed names are predicate anchors, never paint —
90                // recognizing them would emit their ranges as spans.
91                let names: Vec<String> = config
92                    .query
93                    .capture_names()
94                    .iter()
95                    .map(|s| s.to_string())
96                    .filter(|s| !s.starts_with('_'))
97                    .collect();
98                config.configure(&names);
99                Some(Compiled { config, names })
100            })
101            .as_ref()
102    }
103}
104
105#[cfg(feature = "rust")]
106static RUST: Lang = Lang::new(
107    "rust",
108    &["rust", "rs"],
109    tree_sitter_rust::LANGUAGE,
110    include_str!("../queries/rust.scm"),
111);
112#[cfg(feature = "python")]
113static PYTHON: Lang = Lang::new(
114    "python",
115    &["python", "py"],
116    tree_sitter_python::LANGUAGE,
117    include_str!("../queries/python.scm"),
118);
119#[cfg(feature = "typescript")]
120static TYPESCRIPT: Lang = Lang::new(
121    "typescript",
122    &["typescript", "ts"],
123    tree_sitter_typescript::LANGUAGE_TYPESCRIPT,
124    include_str!("../queries/typescript.scm"),
125);
126/// JavaScript rides the TSX grammar: TSX parses JS, and a separate grammar plus
127/// query would buy only the `<`-ambiguity edge cases that JSX and type
128/// assertions disagree on — which a highlighted sample does not hinge on.
129#[cfg(feature = "typescript")]
130static TSX: Lang = Lang::new(
131    "tsx",
132    &["tsx", "jsx", "javascript", "js"],
133    tree_sitter_typescript::LANGUAGE_TSX,
134    include_str!("../queries/tsx.scm"),
135);
136#[cfg(feature = "json")]
137static JSON: Lang = Lang::new(
138    "json",
139    &["json", "jsonc"],
140    tree_sitter_json::LANGUAGE,
141    include_str!("../queries/json.scm"),
142);
143#[cfg(feature = "go")]
144static GO: Lang = Lang::new(
145    "go",
146    &["go", "golang"],
147    tree_sitter_go::LANGUAGE,
148    include_str!("../queries/go.scm"),
149);
150#[cfg(feature = "bash")]
151static BASH: Lang = Lang::new(
152    "bash",
153    &["bash", "sh", "shell", "zsh", "console"],
154    tree_sitter_bash::LANGUAGE,
155    include_str!("../queries/bash.scm"),
156);
157#[cfg(feature = "toml")]
158static TOML: Lang = Lang::new(
159    "toml",
160    &["toml"],
161    tree_sitter_toml_ng::LANGUAGE,
162    include_str!("../queries/toml.scm"),
163);
164
165/// A slice rather than an array: its length is whatever the enabled features
166/// add up to, and an app that highlights one language compiles one grammar.
167pub static LANGS: &[&Lang] = &[
168    #[cfg(feature = "rust")]
169    &RUST,
170    #[cfg(feature = "python")]
171    &PYTHON,
172    #[cfg(feature = "typescript")]
173    &TYPESCRIPT,
174    #[cfg(feature = "typescript")]
175    &TSX,
176    #[cfg(feature = "json")]
177    &JSON,
178    #[cfg(feature = "go")]
179    &GO,
180    #[cfg(feature = "bash")]
181    &BASH,
182    #[cfg(feature = "toml")]
183    &TOML,
184];
185
186/// Find the language a fence tag names. Tags are the raw first word of the
187/// fence info string — `rust {.numberLines}`, `rust,foo`, `Rust` — so they
188/// are trimmed at the first space or comma and case-folded before lookup.
189pub fn resolve(tag: &str) -> Option<&'static Lang> {
190    let tag = tag
191        .split([' ', ','])
192        .next()
193        .unwrap_or("")
194        .to_ascii_lowercase();
195    LANGS
196        .iter()
197        .copied()
198        .find(|l| l.aliases.contains(&tag.as_str()))
199}
200
201/// Map a tree-sitter highlight capture name onto the bezel vocabulary. Names
202/// with no slot degrade to [`HighlightKind::Variable`], which the palettes
203/// paint in the text color — an unknown capture reads as plain text.
204pub fn kind_of(name: &str) -> HighlightKind {
205    match name {
206        "comment" | "comment.documentation" => HighlightKind::Comment,
207        "keyword"
208        | "keyword.function"
209        | "keyword.return"
210        | "keyword.operator"
211        | "keyword.conditional"
212        | "keyword.conditional.ternary"
213        | "keyword.coroutine"
214        | "keyword.directive"
215        | "keyword.exception"
216        | "keyword.import"
217        | "keyword.modifier"
218        | "keyword.repeat"
219        | "keyword.type" => HighlightKind::Keyword,
220        "string" => HighlightKind::String,
221        "string.special" | "string.special.key" | "string.special.url" | "string.regexp"
222        | "character.special" => HighlightKind::StringSpecial,
223        "escape" | "string.escape" => HighlightKind::Escape,
224        "number" => HighlightKind::Number,
225        "boolean" => HighlightKind::Boolean,
226        "type" | "type.interface" => HighlightKind::TypeName,
227        "type.builtin" => HighlightKind::TypeBuiltin,
228        "constructor" => HighlightKind::Constructor,
229        "function" | "function.method" | "function.method.call" | "function.call" => {
230            HighlightKind::Function
231        }
232        "function.builtin" => HighlightKind::FunctionBuiltin,
233        "macro" | "function.macro" => HighlightKind::MacroName,
234        "property" | "property.definition" | "variable.member" => HighlightKind::Property,
235        "constant" | "constant.builtin" | "module" | "module.builtin" => HighlightKind::Constant,
236        "variable" | "variable.builtin" => HighlightKind::Variable,
237        "variable.special" | "self" => HighlightKind::VariableSpecial,
238        "variable.parameter" | "parameter" => HighlightKind::Parameter,
239        "operator" => HighlightKind::Operator,
240        "punctuation" | "punctuation.bracket" | "punctuation.delimiter" | "punctuation.special" => {
241            HighlightKind::Punctuation
242        }
243        "tag" | "tag.builtin" => HighlightKind::Tag,
244        "tag.delimiter" => HighlightKind::Punctuation,
245        "attribute" | "tag.attribute" => HighlightKind::Attribute,
246        "label" => HighlightKind::Label,
247        "invalid" => HighlightKind::Invalid,
248        _ => HighlightKind::Variable,
249    }
250}