Skip to main content

lattice_snippet/
load.rs

1//! TextMate JSON loader -- the format VS Code uses and
2//! `friendly-snippets` ships in. Reading a friendly-snippets
3//! pack is a matter of pointing at its JSON files; no
4//! conversion or shim code.
5//!
6//! Format:
7//!
8//! ```json
9//! {
10//!     "for loop": {
11//!         "prefix": "for",
12//!         "body": [
13//!             "for ${1:i} in ${2:iter} {",
14//!             "\t$0",
15//!             "}"
16//!         ],
17//!         "description": "for-in loop"
18//!     },
19//!     "impl Display": {
20//!         "prefix": ["impl_display", "displ"],
21//!         "body": ["impl Display for ${1:Ty} { ... }"],
22//!         "description": "implement Display"
23//!     }
24//! }
25//! ```
26//!
27//! - `prefix`: string OR string array.
28//! - `body`: string OR string array (joined with `\n`).
29//! - `description`: optional string.
30//! - `scope`: optional string (comma-separated source scopes).
31//!
32//! Top-level keys are snippet names (free-form). Unknown
33//! fields are ignored -- forward-compatible with VS Code's
34//! occasional extensions.
35
36use serde::Deserialize;
37
38use crate::parse;
39use crate::registry::Snippet;
40
41/// Snippet load error. Wraps JSON / parse failures so callers
42/// can surface a clear "snippet pack X failed" message.
43#[derive(Debug, thiserror::Error)]
44pub enum LoadError {
45    #[error("JSON parse: {0}")]
46    Json(#[from] serde_json::Error),
47    #[error("snippet body parse for `{name}`: {error}")]
48    Body {
49        name: String,
50        #[source]
51        error: parse::ParseError,
52    },
53}
54
55/// Parse a friendly-snippets-style JSON pack. Each entry
56/// becomes a [`Snippet`]; the body is parsed eagerly so the
57/// per-keystroke `gen:snippet` source doesn't pay re-parse
58/// cost.
59pub fn load_pack(json: &serde_json::Value) -> Result<Vec<Snippet>, LoadError> {
60    let raw: std::collections::BTreeMap<String, RawSnippet> = serde_json::from_value(json.clone())?;
61    let mut out: Vec<Snippet> = Vec::new();
62    for (name, r) in raw {
63        let prefixes = match r.prefix {
64            StringOrArray::String(s) => vec![s],
65            StringOrArray::Array(v) => v,
66        };
67        let body_str = match r.body {
68            StringOrArray::String(s) => s,
69            StringOrArray::Array(v) => v.join("\n"),
70        };
71        let body = parse::parse(&body_str).map_err(|error| LoadError::Body {
72            name: name.clone(),
73            error,
74        })?;
75        out.push(Snippet {
76            name,
77            prefixes,
78            body,
79            description: r.description,
80            scope: r.scope.unwrap_or_default(),
81        });
82    }
83    Ok(out)
84}
85
86/// Convenience: parse a JSON string directly. Most loader
87/// call sites read a file then route through here.
88pub fn load_pack_from_str(json: &str) -> Result<Vec<Snippet>, LoadError> {
89    let v: serde_json::Value = serde_json::from_str(json)?;
90    load_pack(&v)
91}
92
93#[derive(Debug, Deserialize)]
94struct RawSnippet {
95    prefix: StringOrArray,
96    body: StringOrArray,
97    #[serde(default)]
98    description: Option<String>,
99    #[serde(default)]
100    scope: Option<String>,
101}
102
103#[derive(Debug, Deserialize)]
104#[serde(untagged)]
105enum StringOrArray {
106    String(String),
107    Array(Vec<String>),
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn parses_simple_string_prefix_and_body() {
116        let json = r#"{
117            "for loop": {
118                "prefix": "for",
119                "body": "for ${1:i} in ${2:iter} {}",
120                "description": "for-in loop"
121            }
122        }"#;
123        let snips = load_pack_from_str(json).unwrap();
124        assert_eq!(snips.len(), 1);
125        let s = &snips[0];
126        assert_eq!(s.name, "for loop");
127        assert_eq!(s.prefixes, vec!["for"]);
128        assert_eq!(s.description.as_deref(), Some("for-in loop"));
129    }
130
131    #[test]
132    fn parses_array_prefix_and_body() {
133        let json = r#"{
134            "impl Display": {
135                "prefix": ["impl_display", "displ"],
136                "body": [
137                    "impl Display for ${1:Ty} {",
138                    "\tfn fmt(&self, f: &mut Formatter) -> Result {",
139                    "\t\twrite!(f, \"${2}\")",
140                    "\t}",
141                    "}"
142                ]
143            }
144        }"#;
145        let snips = load_pack_from_str(json).unwrap();
146        let s = &snips[0];
147        assert_eq!(s.prefixes, vec!["impl_display", "displ"]);
148        // Body parses without errors -- placeholders / tabstops
149        // present.
150        assert!(!s.body.is_empty());
151    }
152
153    #[test]
154    fn unknown_top_level_fields_are_ignored() {
155        let json = r#"{
156            "x": {
157                "prefix": "x",
158                "body": "x",
159                "future_field": 42
160            }
161        }"#;
162        let snips = load_pack_from_str(json).unwrap();
163        assert_eq!(snips.len(), 1);
164    }
165
166    #[test]
167    fn missing_required_field_errors() {
168        let json = r#"{ "broken": { "prefix": "br" } }"#;
169        let result = load_pack_from_str(json);
170        assert!(matches!(result, Err(LoadError::Json(_))));
171    }
172
173    #[test]
174    fn scope_field_is_carried() {
175        let json = r#"{
176            "for": {
177                "prefix": "for",
178                "body": "for $1",
179                "scope": "source.rust,source.markdown.injection.rust"
180            }
181        }"#;
182        let snips = load_pack_from_str(json).unwrap();
183        assert_eq!(snips[0].scope, "source.rust,source.markdown.injection.rust");
184    }
185
186    #[test]
187    fn empty_pack_loads_to_empty_vec() {
188        let snips = load_pack_from_str("{}").unwrap();
189        assert!(snips.is_empty());
190    }
191
192    #[test]
193    fn friendly_snippets_for_loop_shape_round_trips() {
194        // Slice of a real friendly-snippets entry shape.
195        let json = r#"{
196            "For Range Loop": {
197                "prefix": ["for", "for-range"],
198                "body": [
199                    "for ${1:i} in ${2:iter} {",
200                    "\t$0",
201                    "}"
202                ],
203                "description": "Iterate over a range using a for loop."
204            }
205        }"#;
206        let snips = load_pack_from_str(json).unwrap();
207        let s = &snips[0];
208        assert_eq!(s.prefixes.len(), 2);
209        // Render to verify parser + render round-trip.
210        let r = crate::render::render(&s.body, &crate::variables::VariableContext::default());
211        assert_eq!(r.text, "for i in iter {\n\t\n}");
212        assert_eq!(r.tabstops.len(), 3);
213    }
214}