Skip to main content

rustik_highlight/
registry.rs

1//! Grammar and theme registry.
2//!
3//! A registry is the lookup layer between user-facing names, file paths, first
4//! lines, and the compiled grammars/themes used by the highlighter. It keeps
5//! inference policy out of tokenization so callers can load syntaxes once and
6//! cheaply select the right one for each buffer.
7
8use std::path::Path;
9use std::str::FromStr;
10
11use crate::Error;
12use crate::grammar::{Grammar, PLAIN_TEXT_NAME};
13use crate::theme::Theme;
14
15/// A reusable registry of compiled grammars and themes.
16#[derive(Debug, Default)]
17pub struct Registry {
18    /// Registered grammars.
19    pub grammars: Vec<Grammar>,
20    /// Registered themes.
21    pub themes: Vec<Theme>,
22}
23
24/// Inputs used to select a grammar from a registry.
25#[derive(Clone, Copy, Debug, Default)]
26pub struct GrammarQuery<'a> {
27    /// Explicit syntax name, root scope, extension, or file-type alias.
28    pub syntax: Option<&'a str>,
29    /// Source path used for file-name and extension inference.
30    pub path: Option<&'a Path>,
31    /// First source line used for shebang and modeline inference.
32    pub first_line: Option<&'a str>,
33}
34
35impl Registry {
36    /// Creates an empty registry.
37    pub fn new() -> Self {
38        Self::default()
39    }
40
41    /// Creates an empty registry with a plain-text fallback grammar.
42    pub fn with_plain_text() -> Self {
43        let mut registry = Self::new();
44        registry.add_grammar(Grammar::plain_text());
45        registry
46    }
47
48    /// Creates an empty registry with the built-in fast grammars.
49    pub fn with_builtin_syntaxes() -> Self {
50        let mut registry = Self::with_plain_text();
51        registry.add_grammar(Grammar::json());
52        registry
53    }
54
55    /// Adds a compiled grammar.
56    pub fn add_grammar(&mut self, grammar: Grammar) {
57        self.grammars.push(grammar);
58    }
59
60    /// Adds the dedicated fast JSON grammar.
61    pub fn add_json(&mut self) {
62        self.add_grammar(Grammar::json());
63    }
64
65    /// Parses, compiles, and adds a grammar from JSON.
66    pub fn add_grammar_json(&mut self, input: &str) -> Result<(), Error> {
67        let grammar = Grammar::from_str(input)?;
68        self.add_grammar(grammar);
69        Ok(())
70    }
71
72    /// Adds a compiled theme.
73    pub fn add_theme(&mut self, theme: Theme) {
74        self.themes.push(theme);
75    }
76
77    /// Parses, compiles, and adds a theme from JSON.
78    pub fn add_theme_json(&mut self, input: &str) -> Result<(), Error> {
79        let theme = Theme::from_str(input)?;
80        self.add_theme(theme);
81        Ok(())
82    }
83
84    /// Finds a grammar by name, root scope, extension, or file-type alias.
85    pub fn grammar_by_name(&self, name: &str) -> Result<&Grammar, Error> {
86        self.grammars
87            .iter()
88            .find(|grammar| grammar.matches_name(name))
89            .ok_or_else(|| Error::UnknownSyntax(name.to_owned()))
90    }
91
92    /// Finds a theme by name.
93    pub fn theme_by_name(&self, name: &str) -> Result<&Theme, Error> {
94        self.themes
95            .iter()
96            .find(|theme| theme.name.eq_ignore_ascii_case(name))
97            .ok_or_else(|| Error::UnknownTheme(name.to_owned()))
98    }
99
100    /// Infers a grammar from explicit syntax, path, extension, and first line.
101    pub fn query(&self, query: GrammarQuery<'_>) -> Result<&Grammar, Error> {
102        if let Some(syntax) = query.syntax {
103            return self.grammar_by_name(syntax);
104        }
105        if let Some(path) = query.path
106            && let Some(grammar) = self
107                .grammars
108                .iter()
109                .find(|grammar| grammar.matches_path(path))
110        {
111            return Ok(grammar);
112        }
113        if let Some(first_line) = query.first_line
114            && let Some(grammar) = self
115                .grammars
116                .iter()
117                .find(|grammar| grammar.matches_first_line(first_line))
118        {
119            return Ok(grammar);
120        }
121        self.grammar_by_name(PLAIN_TEXT_NAME)
122    }
123}