Skip to main content

lattice_snippet/
registry.rs

1//! Per-language snippet registry. Stores parsed bodies
2//! keyed by trigger prefix; the host's `gen:snippet` source
3//! consults this registry per-popup-trigger.
4
5use std::collections::HashMap;
6
7use crate::token::SnippetBody;
8
9/// One snippet entry. `prefixes` may have multiple entries
10/// (TextMate JSON `"prefix": ["loop", "fr"]` form). Body is
11/// pre-parsed; the load path runs the parser once at startup.
12#[derive(Debug, Clone)]
13pub struct Snippet {
14    pub name: String,
15    pub prefixes: Vec<String>,
16    pub body: SnippetBody,
17    pub description: Option<String>,
18    /// Comma-separated source-scope filter (e.g.
19    /// `"source.rust,source.markdown"`). Empty = all-language.
20    /// Host maps `source.<lang>` to the buffer's tree-sitter
21    /// language at filter time.
22    pub scope: String,
23}
24
25/// Display-only metadata for a snippet -- the bits the
26/// completion popup row needs without pulling in the body.
27#[derive(Debug, Clone)]
28pub struct SnippetMeta {
29    pub name: String,
30    pub prefix: String,
31    pub description: Option<String>,
32}
33
34/// Per-language store. Snippets index by every prefix they
35/// register (multi-prefix snippets register under each).
36#[derive(Debug, Default, Clone)]
37pub struct SnippetRegistry {
38    /// `language -> { prefix -> snippet }`. v1 is per-language;
39    /// scope-expression filtering (e.g. `source.markdown.injection.rust`)
40    /// rides on the same shape once the major-mode plumbing exists.
41    by_language: HashMap<String, HashMap<String, Vec<Snippet>>>,
42    /// All snippets keyed by name -- handy for `:reload-snippets`
43    /// dedup and `:describe-snippet <name>` lookups.
44    all_by_name: HashMap<String, Snippet>,
45}
46
47impl SnippetRegistry {
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Insert a snippet for a given language. When the snippet
53    /// has multiple prefixes, registers under each. Languages
54    /// are matched verbatim against the host's scope -> language
55    /// mapping; future scope-expression support extends this.
56    pub fn insert(&mut self, language: &str, snippet: Snippet) {
57        let by_prefix = self.by_language.entry(language.to_string()).or_default();
58        for prefix in &snippet.prefixes {
59            by_prefix
60                .entry(prefix.clone())
61                .or_default()
62                .push(snippet.clone());
63        }
64        self.all_by_name.insert(snippet.name.clone(), snippet);
65    }
66
67    /// Snippets matching `prefix` for `language`. Walks the
68    /// per-language index then a `*` (any-language) index. Each
69    /// snippet appears once even when its prefix slot has
70    /// multiple registrations (different snippets sharing a
71    /// prefix is allowed -- popular for "if-else" vs "if" both
72    /// triggering on `if`).
73    pub fn lookup(&self, language: &str, prefix: &str) -> Vec<&Snippet> {
74        let mut out: Vec<&Snippet> = Vec::new();
75        if let Some(by_prefix) = self.by_language.get(language)
76            && let Some(snips) = by_prefix.get(prefix)
77        {
78            out.extend(snips.iter());
79        }
80        if let Some(by_prefix) = self.by_language.get("*")
81            && let Some(snips) = by_prefix.get(prefix)
82        {
83            out.extend(snips.iter());
84        }
85        out
86    }
87
88    /// Every snippet whose prefix STARTS with the given
89    /// `query` (case-insensitive) for the given language.
90    /// Used by the `gen:snippet` source to populate the
91    /// completion popup -- the host's matcher takes over
92    /// from there.
93    pub fn matching_prefix<'a>(&'a self, language: &str, query: &str) -> Vec<&'a Snippet> {
94        let q = query.to_lowercase();
95        let mut out: Vec<&'a Snippet> = Vec::new();
96        let mut seen: std::collections::HashSet<&'a str> = std::collections::HashSet::new();
97        for source_lang in [language, "*"] {
98            let Some(by_prefix) = self.by_language.get(source_lang) else {
99                continue;
100            };
101            for (prefix, snips) in by_prefix {
102                if !prefix.to_lowercase().starts_with(&q) {
103                    continue;
104                }
105                for s in snips {
106                    if seen.insert(s.name.as_str()) {
107                        out.push(s);
108                    }
109                }
110            }
111        }
112        out
113    }
114
115    pub fn by_name(&self, name: &str) -> Option<&Snippet> {
116        self.all_by_name.get(name)
117    }
118
119    pub fn len(&self) -> usize {
120        self.all_by_name.len()
121    }
122
123    pub fn is_empty(&self) -> bool {
124        self.all_by_name.is_empty()
125    }
126
127    /// Languages with at least one registered snippet.
128    pub fn languages(&self) -> Vec<&str> {
129        self.by_language.keys().map(|s| s.as_str()).collect()
130    }
131
132    /// Display-only metadata view -- `:list-snippets` /
133    /// `:describe-snippet` consume this without forcing a
134    /// body clone.
135    pub fn meta_for_language(&self, language: &str) -> Vec<SnippetMeta> {
136        let mut out: Vec<SnippetMeta> = Vec::new();
137        if let Some(by_prefix) = self.by_language.get(language) {
138            for (prefix, snips) in by_prefix {
139                for s in snips {
140                    out.push(SnippetMeta {
141                        name: s.name.clone(),
142                        prefix: prefix.clone(),
143                        description: s.description.clone(),
144                    });
145                }
146            }
147        }
148        out
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::parse;
156
157    fn snip(name: &str, prefix: &str, body: &str) -> Snippet {
158        Snippet {
159            name: name.into(),
160            prefixes: vec![prefix.into()],
161            body: parse::parse(body).unwrap(),
162            description: None,
163            scope: String::new(),
164        }
165    }
166
167    #[test]
168    fn lookup_returns_matching_snippet() {
169        let mut r = SnippetRegistry::new();
170        r.insert(
171            "rust",
172            snip("for-loop", "for", "for ${1:i} in ${2:iter} {}"),
173        );
174        let hits = r.lookup("rust", "for");
175        assert_eq!(hits.len(), 1);
176        assert_eq!(hits[0].name, "for-loop");
177    }
178
179    #[test]
180    fn matching_prefix_walks_per_language_then_star() {
181        let mut r = SnippetRegistry::new();
182        r.insert("rust", snip("rust-for", "for", "for $1"));
183        r.insert("*", snip("anywhere", "fn", "fn $1"));
184        let hits = r.matching_prefix("rust", "f");
185        let names: Vec<&str> = hits.iter().map(|s| s.name.as_str()).collect();
186        assert!(names.contains(&"rust-for"));
187        assert!(names.contains(&"anywhere"));
188    }
189
190    #[test]
191    fn multi_prefix_snippets_register_under_each() {
192        let mut r = SnippetRegistry::new();
193        let mut s = snip("for-or-fr", "for", "for $1");
194        s.prefixes = vec!["for".into(), "fr".into()];
195        r.insert("rust", s);
196        assert_eq!(r.lookup("rust", "for").len(), 1);
197        assert_eq!(r.lookup("rust", "fr").len(), 1);
198    }
199
200    #[test]
201    fn matching_prefix_dedups_by_snippet_name() {
202        let mut r = SnippetRegistry::new();
203        let mut s = snip("for-or-fr", "for", "for $1");
204        s.prefixes = vec!["for".into(), "fr".into()];
205        r.insert("rust", s);
206        // Query "f" matches both "for" and "fr" prefix slots
207        // pointing at the same snippet -- dedup via name.
208        let hits = r.matching_prefix("rust", "f");
209        assert_eq!(hits.len(), 1);
210    }
211
212    #[test]
213    fn meta_for_language_omits_body() {
214        let mut r = SnippetRegistry::new();
215        r.insert("rust", snip("for", "for", "for $1"));
216        let meta = r.meta_for_language("rust");
217        assert_eq!(meta.len(), 1);
218        assert_eq!(meta[0].name, "for");
219        assert_eq!(meta[0].prefix, "for");
220    }
221}