1use anyhow::{anyhow, Context, Result};
2use smallvec::SmallVec;
3use std::{collections::BTreeMap, ops::Range};
4
5#[derive(Default)]
6pub struct Snippet {
7 pub text: String,
8 pub tabstops: Vec<TabStop>,
9}
10
11type TabStop = SmallVec<[Range<isize>; 2]>;
12
13impl Snippet {
14 pub fn parse(source: &str) -> Result<Self> {
15 let mut text = String::with_capacity(source.len());
16 let mut tabstops = BTreeMap::new();
17 parse_snippet(source, false, &mut text, &mut tabstops)
18 .context("failed to parse snippet")?;
19
20 let last_tabstop = tabstops
21 .remove(&0)
22 .unwrap_or_else(|| SmallVec::from_iter([text.len() as isize..text.len() as isize]));
23 Ok(Snippet {
24 text,
25 tabstops: tabstops.into_values().chain(Some(last_tabstop)).collect(),
26 })
27 }
28}
29
30fn parse_snippet<'a>(
31 mut source: &'a str,
32 nested: bool,
33 text: &mut String,
34 tabstops: &mut BTreeMap<usize, TabStop>,
35) -> Result<&'a str> {
36 loop {
37 match source.chars().next() {
38 None => return Ok(""),
39 Some('$') => {
40 source = parse_tabstop(&source[1..], text, tabstops)?;
41 }
42 Some('}') => {
43 if nested {
44 return Ok(source);
45 } else {
46 text.push('}');
47 source = &source[1..];
48 }
49 }
50 Some(_) => {
51 let chunk_end = source.find(&['}', '$']).unwrap_or(source.len());
52 let (chunk, rest) = source.split_at(chunk_end);
53 text.push_str(chunk);
54 source = rest;
55 }
56 }
57 }
58}
59
60fn parse_tabstop<'a>(
61 mut source: &'a str,
62 text: &mut String,
63 tabstops: &mut BTreeMap<usize, TabStop>,
64) -> Result<&'a str> {
65 let tabstop_start = text.len();
66 let tabstop_index;
67 if source.chars().next() == Some('{') {
68 let (index, rest) = parse_int(&source[1..])?;
69 tabstop_index = index;
70 source = rest;
71
72 if source.chars().next() == Some(':') {
73 source = parse_snippet(&source[1..], true, text, tabstops)?;
74 }
75
76 if source.chars().next() == Some('}') {
77 source = &source[1..];
78 } else {
79 return Err(anyhow!("expected a closing brace"));
80 }
81 } else {
82 let (index, rest) = parse_int(&source)?;
83 tabstop_index = index;
84 source = rest;
85 }
86
87 tabstops
88 .entry(tabstop_index)
89 .or_default()
90 .push(tabstop_start as isize..text.len() as isize);
91 Ok(source)
92}
93
94fn parse_int(source: &str) -> Result<(usize, &str)> {
95 let len = source
96 .find(|c: char| !c.is_ascii_digit())
97 .unwrap_or(source.len());
98 if len == 0 {
99 return Err(anyhow!("expected an integer"));
100 }
101 let (prefix, suffix) = source.split_at(len);
102 Ok((prefix.parse()?, suffix))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn test_snippet_without_tabstops() {
111 let snippet = Snippet::parse("one-two-three").unwrap();
112 assert_eq!(snippet.text, "one-two-three");
113 assert_eq!(tabstops(&snippet), &[vec![13..13]]);
114 }
115
116 #[test]
117 fn test_snippet_with_tabstops() {
118 let snippet = Snippet::parse("one$1two").unwrap();
119 assert_eq!(snippet.text, "onetwo");
120 assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]);
121
122 // Multi-digit numbers
123 let snippet = Snippet::parse("one$123-$99-two").unwrap();
124 assert_eq!(snippet.text, "one--two");
125 assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]);
126 }
127
128 #[test]
129 fn test_snippet_with_explicit_final_tabstop() {
130 let snippet = Snippet::parse(r#"<div class="$1">$0</div>"#).unwrap();
131 assert_eq!(snippet.text, r#"<div class=""></div>"#);
132 assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]);
133 }
134
135 #[test]
136 fn test_snippet_with_placeholders() {
137 let snippet = Snippet::parse("one${1:two}three${2:four}").unwrap();
138 assert_eq!(snippet.text, "onetwothreefour");
139 assert_eq!(
140 tabstops(&snippet),
141 &[vec![3..6], vec![11..15], vec![15..15]]
142 );
143 }
144
145 #[test]
146 fn test_snippet_with_nested_placeholders() {
147 let snippet = Snippet::parse(
148 "for (${1:var ${2:i} = 0; ${2:i} < ${3:${4:array}.length}; ${2:i}++}) {$0}",
149 )
150 .unwrap();
151 assert_eq!(snippet.text, "for (var i = 0; i < array.length; i++) {}");
152 assert_eq!(
153 tabstops(&snippet),
154 &[
155 vec![5..37],
156 vec![9..10, 16..17, 34..35],
157 vec![20..32],
158 vec![20..25],
159 vec![40..40],
160 ]
161 );
162 }
163
164 fn tabstops(snippet: &Snippet) -> Vec<Vec<Range<isize>>> {
165 snippet.tabstops.iter().map(|t| t.to_vec()).collect()
166 }
167}