@@ -0,0 +1,367 @@
1+use anyhow::{anyhow, Result};
2+
3+use crate::{util::lsp_pos_to_pos, OffsetEncoding};
4+
5+#[derive(Debug, PartialEq, Eq)]
6+pub enum CaseChange {
7+ Upcase,
8+ Downcase,
9+ Capitalize,
10+}
11+
12+#[derive(Debug, PartialEq, Eq)]
13+pub enum FormatItem<'a> {
14+ Text(&'a str),
15+ Capture(usize),
16+ CaseChange(usize, CaseChange),
17+ Conditional(usize, Option<&'a str>, Option<&'a str>),
18+}
19+
20+#[derive(Debug, PartialEq, Eq)]
21+pub struct Regex<'a> {
22+ value: &'a str,
23+ replacement: Vec<FormatItem<'a>>,
24+ options: Option<&'a str>,
25+}
26+
27+#[derive(Debug, PartialEq, Eq)]
28+pub enum SnippetElement<'a> {
29+ Tabstop {
30+ tabstop: usize,
31+ },
32+ Placeholder {
33+ tabstop: usize,
34+ value: Box<SnippetElement<'a>>,
35+ },
36+ Choice {
37+ tabstop: usize,
38+ choices: Vec<&'a str>,
39+ },
40+ Variable {
41+ name: &'a str,
42+ default: Option<&'a str>,
43+ regex: Option<Regex<'a>>,
44+ },
45+ Text(&'a str),
46+}
47+
48+#[derive(Debug, PartialEq, Eq)]
49+pub struct Snippet<'a> {
50+ elements: Vec<SnippetElement<'a>>,
51+}
52+
53+pub fn parse<'a>(s: &'a str) -> Result<Snippet<'a>> {
54+ parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest))
55+}
56+
57+mod parser {
58+ use helix_core::regex;
59+ use once_cell::sync::Lazy;
60+
61+ use helix_parsec::*;
62+
63+ use super::{CaseChange, FormatItem, Regex, Snippet, SnippetElement};
64+
65+ /*
66+ https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax
67+
68+ any ::= tabstop | placeholder | choice | variable | text
69+ tabstop ::= '$' int | '${' int '}'
70+ placeholder ::= '${' int ':' any '}'
71+ choice ::= '${' int '|' text (',' text)* '|}'
72+ variable ::= '$' var | '${' var }'
73+ | '${' var ':' any '}'
74+ | '${' var '/' regex '/' (format | text)+ '/' options '}'
75+ format ::= '$' int | '${' int '}'
76+ | '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}'
77+ | '${' int ':+' if '}'
78+ | '${' int ':?' if ':' else '}'
79+ | '${' int ':-' else '}' | '${' int ':' else '}'
80+ regex ::= Regular Expression value (ctor-string)
81+ options ::= Regular Expression option (ctor-options)
82+ var ::= [_a-zA-Z] [_a-zA-Z0-9]*
83+ int ::= [0-9]+
84+ text ::= .*
85+ if ::= text
86+ else ::= text
87+ */
88+
89+ static DIGIT: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"^[0-9]+").unwrap());
90+ static VARIABLE: Lazy<regex::Regex> =
91+ Lazy::new(|| regex::Regex::new(r"^[_a-zA-Z][_a-zA-Z0-9]*").unwrap());
92+ static TEXT: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"^[^\$]+").unwrap());
93+
94+ fn var<'a>() -> impl Parser<'a, Output = &'a str> {
95+ pattern(&VARIABLE)
96+ }
97+
98+ fn digit<'a>() -> impl Parser<'a, Output = usize> {
99+ filter_map(pattern(&DIGIT), |s| s.parse().ok())
100+ }
101+
102+ fn case_change<'a>() -> impl Parser<'a, Output = CaseChange> {
103+ use CaseChange::*;
104+
105+ choice!(
106+ map("upcase", |_| Upcase),
107+ map("downcase", |_| Downcase),
108+ map("capitalize", |_| Capitalize),
109+ )
110+ }
111+
112+ fn format<'a>() -> impl Parser<'a, Output = FormatItem<'a>> {
113+ use FormatItem::*;
114+
115+ choice!(
116+ // '$' int
117+ map(right("$", digit()), Capture),
118+ // '${' int '}'
119+ map(seq!("${", digit(), "}"), |seq| Capture(seq.1)),
120+ // '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}'
121+ map(seq!("${", digit(), ":/", case_change(), "}"), |seq| {
122+ CaseChange(seq.1, seq.3)
123+ }),
124+ // '${' int ':+' if '}'
125+ map(
126+ seq!("${", digit(), ":+", take_until(|c| c == '}'), "}"),
127+ |seq| { Conditional(seq.1, Some(seq.3), None) }
128+ ),
129+ // '${' int ':?' if ':' else '}'
130+ map(
131+ seq!(
132+ "${",
133+ digit(),
134+ ":?",
135+ take_until(|c| c == ':'),
136+ ":",
137+ take_until(|c| c == '}'),
138+ "}"
139+ ),
140+ |seq| { Conditional(seq.1, Some(seq.3), Some(seq.5)) }
141+ ),
142+ // '${' int ':-' else '}' | '${' int ':' else '}'
143+ map(
144+ seq!(
145+ "${",
146+ digit(),
147+ ":",
148+ optional("-"),
149+ take_until(|c| c == '}'),
150+ "}"
151+ ),
152+ |seq| { Conditional(seq.1, None, Some(seq.4)) }
153+ ),
154+ // Any text
155+ map(pattern(&TEXT), Text),
156+ )
157+ }
158+
159+ fn regex<'a>() -> impl Parser<'a, Output = Regex<'a>> {
160+ let replacement = reparse_as(take_until(|c| c == '/'), one_or_more(format()));
161+
162+ map(
163+ seq!(
164+ "/",
165+ take_until(|c| c == '/'),
166+ "/",
167+ replacement,
168+ "/",
169+ optional(take_until(|c| c == '}')),
170+ ),
171+ |(_, value, _, replacement, _, options)| Regex {
172+ value,
173+ replacement,
174+ options,
175+ },
176+ )
177+ }
178+
179+ fn tabstop<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
180+ map(
181+ or(
182+ right("$", digit()),
183+ map(seq!("${", digit(), "}"), |values| values.1),
184+ ),
185+ |digit| SnippetElement::Tabstop { tabstop: digit },
186+ )
187+ }
188+
189+ fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
190+ // TODO: why doesn't parse_as work?
191+ // let value = reparse_as(take_until(|c| c == '}'), anything());
192+ let value = filter_map(take_until(|c| c == '}'), |s| {
193+ anything().parse(s).map(|parse_result| parse_result.1).ok()
194+ });
195+
196+ map(seq!("${", digit(), ":", value, "}"), |seq| {
197+ SnippetElement::Placeholder {
198+ tabstop: seq.1,
199+ value: Box::new(seq.3),
200+ }
201+ })
202+ }
203+
204+ fn choice<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
205+ map(
206+ seq!(
207+ "${",
208+ digit(),
209+ "|",
210+ sep(take_until(|c| c == ',' || c == '|'), ","),
211+ "|}",
212+ ),
213+ |seq| SnippetElement::Choice {
214+ tabstop: seq.1,
215+ choices: seq.3,
216+ },
217+ )
218+ }
219+
220+ fn variable<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
221+ choice!(
222+ // $var
223+ map(right("$", var()), |name| SnippetElement::Variable {
224+ name,
225+ default: None,
226+ regex: None,
227+ }),
228+ // ${var:default}
229+ map(
230+ seq!("${", var(), ":", take_until(|c| c == '}'), "}",),
231+ |values| SnippetElement::Variable {
232+ name: values.1,
233+ default: Some(values.3),
234+ regex: None,
235+ }
236+ ),
237+ // ${var/value/format/options}
238+ map(seq!("${", var(), regex(), "}"), |values| {
239+ SnippetElement::Variable {
240+ name: values.1,
241+ default: None,
242+ regex: Some(values.2),
243+ }
244+ }),
245+ )
246+ }
247+
248+ fn text<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
249+ map(pattern(&TEXT), SnippetElement::Text)
250+ }
251+
252+ fn anything<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> {
253+ choice!(tabstop(), placeholder(), choice(), variable(), text())
254+ }
255+
256+ fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> {
257+ map(one_or_more(anything()), |parts| Snippet { elements: parts })
258+ }
259+
260+ pub fn parse(s: &str) -> Result<Snippet, &str> {
261+ snippet().parse(s).map(|(_input, elements)| elements)
262+ }
263+
264+ #[cfg(test)]
265+ mod test {
266+ use super::SnippetElement::*;
267+ use super::*;
268+
269+ #[test]
270+ fn empty_string_is_error() {
271+ assert_eq!(Err(""), parse(""));
272+ }
273+
274+ #[test]
275+ fn parse_placeholders_in_function_call() {
276+ assert_eq!(
277+ Ok(Snippet {
278+ elements: vec![
279+ Text("match("),
280+ Placeholder {
281+ tabstop: 1,
282+ value: Box::new(Text("Arg1")),
283+ },
284+ Text(")")
285+ ]
286+ }),
287+ parse("match(${1:Arg1})")
288+ )
289+ }
290+
291+ #[test]
292+ fn parse_placeholders_in_statement() {
293+ assert_eq!(
294+ Ok(Snippet {
295+ elements: vec![
296+ Text("local "),
297+ Placeholder {
298+ tabstop: 1,
299+ value: Box::new(Text("var")),
300+ },
301+ Text(" = "),
302+ Placeholder {
303+ tabstop: 1,
304+ value: Box::new(Text("value")),
305+ },
306+ ]
307+ }),
308+ parse("local ${1:var} = ${1:value}")
309+ )
310+ }
311+
312+ #[test]
313+ fn parse_all() {
314+ assert_eq!(
315+ Ok(Snippet {
316+ elements: vec![
317+ Text("hello "),
318+ Tabstop { tabstop: 1 },
319+ Tabstop { tabstop: 2 },
320+ Text(" "),
321+ Choice {
322+ tabstop: 1,
323+ choices: vec!["one", "two", "three"]
324+ },
325+ Text(" "),
326+ Variable {
327+ name: "name",
328+ default: Some("foo"),
329+ regex: None
330+ },
331+ Text(" "),
332+ Variable {
333+ name: "var",
334+ default: None,
335+ regex: None
336+ },
337+ Text(" "),
338+ Variable {
339+ name: "TM",
340+ default: None,
341+ regex: None
342+ },
343+ ]
344+ }),
345+ parse("hello $1${2} ${1|one,two,three|} ${name:foo} $var $TM")
346+ );
347+ }
348+
349+ #[test]
350+ fn regex_capture_replace() {
351+ assert_eq!(
352+ Ok(Snippet {
353+ elements: vec![Variable {
354+ name: "TM_FILENAME",
355+ default: None,
356+ regex: Some(Regex {
357+ value: "(.*).+$",
358+ replacement: vec![FormatItem::Capture(1)],
359+ options: None,
360+ }),
361+ }]
362+ }),
363+ parse("${TM_FILENAME/(.*).+$/$1/}")
364+ );
365+ }
366+ }
367+}