on master help sign in
do not fail snippet parsing when control chars are not explicitly escaped
cabb746Pascal Kuthe
helix-lsp/src/snippet.rs+111−38
@@ -11,17 +11,17 @@ pub enum CaseChange {
11}
11}
12
12
13#[derive(Debug, PartialEq, Eq)]
13#[derive(Debug, PartialEq, Eq)]
14−pub enum FormatItem<'a> {
14+pub enum FormatItem {
15 Text(Tendril),
15 Text(Tendril),
16 Capture(usize),
16 Capture(usize),
17 CaseChange(usize, CaseChange),
17 CaseChange(usize, CaseChange),
18− Conditional(usize, Option<&'a str>, Option<&'a str>),
18+ Conditional(usize, Option<Tendril>, Option<Tendril>),
19}
19}
20
20
21#[derive(Debug, PartialEq, Eq)]
21#[derive(Debug, PartialEq, Eq)]
22−pub struct Regex<'a> {
22+pub struct Regex {
23 value: Tendril,
23 value: Tendril,
24− replacement: Vec<FormatItem<'a>>,
24+ replacement: Vec<FormatItem>,
25 options: Tendril,
25 options: Tendril,
26}
26}
27
27
@@ -40,8 +40,8 @@ pub enum SnippetElement<'a> {
40 },
40 },
41 Variable {
41 Variable {
42 name: &'a str,
42 name: &'a str,
43− default: Option<&'a str>,
43+ default: Option<Vec<SnippetElement<'a>>>,
44− regex: Option<Regex<'a>>,
44+ regex: Option<Regex>,
45 },
45 },
46 Text(Tendril),
46 Text(Tendril),
47}
47}
@@ -77,15 +77,20 @@ fn render_elements(
77 *offset += text.chars().count();
77 *offset += text.chars().count();
78 insert.push_str(&text);
78 insert.push_str(&text);
79 }
79 }
80− &Variable {
80+ Variable {
81 name: _,
81 name: _,
82 regex: _,
82 regex: _,
83 r#default,
83 r#default,
84 } => {
84 } => {
85 // TODO: variables. For now, fall back to the default, which defaults to "".
85 // TODO: variables. For now, fall back to the default, which defaults to "".
86+ render_elements(
86− let text = r#default.unwrap_or_default();
87+ r#default.as_deref().unwrap_or_default(),
87− *offset += text.chars().count();
88− insert.push_str(text);
88+ insert,
89+ offset,
90+ tabstops,
91+ newline_with_offset,
92+ include_placeholer,
93+ );
89 }
94 }
90 &Tabstop { tabstop } => {
95 &Tabstop { tabstop } => {
91 tabstops.push((tabstop, (*offset, *offset)));
96 tabstops.push((tabstop, (*offset, *offset)));
@@ -212,25 +217,28 @@ mod parser {
212 }
217 }
213
218
214 const TEXT_ESCAPE_CHARS: &[char] = &['\\', '}', '$'];
219 const TEXT_ESCAPE_CHARS: &[char] = &['\\', '}', '$'];
215− const REPLACE_ESCAPE_CHARS: &[char] = &['\\', '}', '$', '/'];
216− const CHOICE_TEXT_ESCAPE_CHARS: &[char] = &['\\', '}', '$', '|', ','];
220+ const CHOICE_TEXT_ESCAPE_CHARS: &[char] = &['\\', '|', ','];
217
221
222+ fn text<'a>(
223+ escape_chars: &'static [char],
224+ term_chars: &'static [char],
218− fn text<'a>(escape_chars: &'static [char]) -> impl Parser<'a, Output = Tendril> {
225+ ) -> impl Parser<'a, Output = Tendril> {
219 move |input: &'a str| {
226 move |input: &'a str| {
220− let mut chars = input.char_indices();
227+ let mut chars = input.char_indices().peekable();
221 let mut res = Tendril::new();
228 let mut res = Tendril::new();
222 while let Some((i, c)) = chars.next() {
229 while let Some((i, c)) = chars.next() {
223 match c {
230 match c {
224 '\\' => {
231 '\\' => {
225− if let Some((_, c)) = chars.next() {
232+ if let Some(&(_, c)) = chars.peek() {
226 if escape_chars.contains(&c) {
233 if escape_chars.contains(&c) {
234+ chars.next();
227 res.push(c);
235 res.push(c);
228 continue;
236 continue;
229 }
237 }
230 }
238 }
231− return Ok((&input[i..], res));
239+ res.push('\\');
232 }
240 }
233− c if escape_chars.contains(&c) => return Ok((&input[i..], res)),
241+ c if term_chars.contains(&c) => return Ok((&input[i..], res)),
234 c => res.push(c),
242 c => res.push(c),
235 }
243 }
236 }
244 }
@@ -253,7 +261,7 @@ mod parser {
253 )
261 )
254 }
262 }
255
263
256− fn format<'a>() -> impl Parser<'a, Output = FormatItem<'a>> {
264+ fn format<'a>() -> impl Parser<'a, Output = FormatItem> {
257 use FormatItem::*;
265 use FormatItem::*;
258
266
259 choice!(
267 choice!(
@@ -267,7 +275,7 @@ mod parser {
267 }),
275 }),
268 // '${' int ':+' if '}'
276 // '${' int ':+' if '}'
269 map(
277 map(
270− seq!("${", digit(), ":+", take_until(|c| c == '}'), "}"),
278+ seq!("${", digit(), ":+", text(TEXT_ESCAPE_CHARS, &['}']), "}"),
271 |seq| { Conditional(seq.1, Some(seq.3), None) }
279 |seq| { Conditional(seq.1, Some(seq.3), None) }
272 ),
280 ),
273 // '${' int ':?' if ':' else '}'
281 // '${' int ':?' if ':' else '}'
@@ -276,9 +284,9 @@ mod parser {
276 "${",
284 "${",
277 digit(),
285 digit(),
278 ":?",
286 ":?",
279− take_until(|c| c == ':'),
287+ text(TEXT_ESCAPE_CHARS, &[':']),
280 ":",
288 ":",
281− take_until(|c| c == '}'),
289+ text(TEXT_ESCAPE_CHARS, &['}']),
282 "}"
290 "}"
283 ),
291 ),
284 |seq| { Conditional(seq.1, Some(seq.3), Some(seq.5)) }
292 |seq| { Conditional(seq.1, Some(seq.3), Some(seq.5)) }
@@ -290,7 +298,7 @@ mod parser {
290 digit(),
298 digit(),
291 ":",
299 ":",
292 optional("-"),
300 optional("-"),
293− take_until(|c| c == '}'),
301+ text(TEXT_ESCAPE_CHARS, &['}']),
294 "}"
302 "}"
295 ),
303 ),
296 |seq| { Conditional(seq.1, None, Some(seq.4)) }
304 |seq| { Conditional(seq.1, None, Some(seq.4)) }
@@ -298,19 +306,24 @@ mod parser {
298 )
306 )
299 }
307 }
300
308
301− fn regex<'a>() -> impl Parser<'a, Output = Regex<'a>> {
309+ fn regex<'a>() -> impl Parser<'a, Output = Regex> {
302 map(
310 map(
303 seq!(
311 seq!(
304 "/",
312 "/",
305 // TODO parse as ECMAScript and convert to rust regex
313 // TODO parse as ECMAScript and convert to rust regex
306− non_empty(text(&['/', '\\'])),
314+ non_empty(text(&['/'], &['/'])),
307 "/",
315 "/",
308 one_or_more(choice!(
316 one_or_more(choice!(
309 format(),
317 format(),
318+ // text doesn't parse $, if format fails we just accept the $ as text
319+ map("$", |_| FormatItem::Text("$".into())),
310− map(text(REPLACE_ESCAPE_CHARS), FormatItem::Text)
320+ map(text(&['\\', '/'], &['/', '$']), FormatItem::Text),
311 )),
321 )),
312 "/",
322 "/",
323+ // vscode really doesn't allow escaping } here
324+ // so it's impossible to write a regex escape containing a }
325+ // we can consider deviating here and allowing the escape
313− text(&['}', '\\',]),
326+ text(&[], &['}']),
314 ),
327 ),
315 |(_, value, _, replacement, _, options)| Regex {
328 |(_, value, _, replacement, _, options)| Regex {
316 value,
329 value,
@@ -341,7 +354,7 @@ mod parser {
341 // The example there contains both a placeholder text and a nested placeholder
354 // The example there contains both a placeholder text and a nested placeholder
342 // which indicates a list. Looking at the VSCode sourcecode, the placeholder
355 // which indicates a list. Looking at the VSCode sourcecode, the placeholder
343 // is indeed parsed as zero_or_more so the grammar is simply incorrect here
356 // is indeed parsed as zero_or_more so the grammar is simply incorrect here
344− zero_or_more(anything(TEXT_ESCAPE_CHARS)),
357+ zero_or_more(anything(TEXT_ESCAPE_CHARS, true)),
345 "}"
358 "}"
346 ),
359 ),
347 |seq| SnippetElement::Placeholder {
360 |seq| SnippetElement::Placeholder {
@@ -357,7 +370,7 @@ mod parser {
357 "${",
370 "${",
358 digit(),
371 digit(),
359 "|",
372 "|",
360− sep(text(CHOICE_TEXT_ESCAPE_CHARS), ","),
373+ sep(text(CHOICE_TEXT_ESCAPE_CHARS, &['|', ',']), ","),
361 "|}",
374 "|}",
362 ),
375 ),
363 |seq| SnippetElement::Choice {
376 |seq| SnippetElement::Choice {
@@ -377,7 +390,13 @@ mod parser {
377 }),
390 }),
378 // ${var:default}
391 // ${var:default}
379 map(
392 map(
380− seq!("${", var(), ":", take_until(|c| c == '}'), "}",),
393+ seq!(
394+ "${",
395+ var(),
396+ ":",
397+ zero_or_more(anything(TEXT_ESCAPE_CHARS, true)),
398+ "}",
399+ ),
381 |values| SnippetElement::Variable {
400 |values| SnippetElement::Variable {
382 name: values.1,
401 name: values.1,
383 default: Some(values.3),
402 default: Some(values.3),
@@ -395,22 +414,27 @@ mod parser {
395 )
414 )
396 }
415 }
397
416
417+ fn anything<'a>(
418+ escape_chars: &'static [char],
419+ end_at_brace: bool,
398− fn anything<'a>(escape_chars: &'static [char]) -> impl Parser<'a, Output = SnippetElement<'a>> {
420+ ) -> impl Parser<'a, Output = SnippetElement<'a>> {
421+ let term_chars: &[_] = if end_at_brace { &['$', '}'] } else { &['$'] };
399 move |input: &'a str| {
422 move |input: &'a str| {
400 let parser = choice!(
423 let parser = choice!(
401 tabstop(),
424 tabstop(),
402 placeholder(),
425 placeholder(),
403 choice(),
426 choice(),
404 variable(),
427 variable(),
428+ map("$", |_| SnippetElement::Text("$".into())),
405− map(text(escape_chars), SnippetElement::Text)
429+ map(text(escape_chars, term_chars), SnippetElement::Text),
406 );
430 );
407 parser.parse(input)
431 parser.parse(input)
408 }
432 }
409 }
433 }
410
434
411 fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> {
435 fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> {
412− map(one_or_more(anything(TEXT_ESCAPE_CHARS)), |parts| Snippet {
436+ map(one_or_more(anything(TEXT_ESCAPE_CHARS, false)), |parts| {
413− elements: parts,
437+ Snippet { elements: parts }
414 })
438 })
415 }
439 }
416
440
@@ -452,8 +476,13 @@ mod parser {
452 }
476 }
453
477
454 #[test]
478 #[test]
455− fn parse_unterminated_placeholder_error() {
479+ fn unterminated_placeholder() {
480+ assert_eq!(
481+ Ok(Snippet {
482+ elements: vec![Text("match(".into()), Text("$".into()), Text("{1:)".into())]
483+ }),
456− assert_eq!(Err("${1:)"), parse("match(${1:)"))
484+ parse("match(${1:)")
485+ )
457 }
486 }
458
487
459 #[test]
488 #[test]
@@ -542,7 +571,7 @@ mod parser {
542 Text(" ".into()),
571 Text(" ".into()),
543 Variable {
572 Variable {
544 name: "name",
573 name: "name",
545− default: Some("foo"),
574+ default: Some(vec![Text("foo".into())]),
546 regex: None
575 regex: None
547 },
576 },
548 Text(" ".into()),
577 Text(" ".into()),
@@ -572,12 +601,56 @@ mod parser {
572 default: None,
601 default: None,
573 regex: Some(Regex {
602 regex: Some(Regex {
574 value: "(.*).+$".into(),
603 value: "(.*).+$".into(),
575− replacement: vec![FormatItem::Capture(1)],
604+ replacement: vec![FormatItem::Capture(1), FormatItem::Text("$".into())],
576 options: Tendril::new(),
605 options: Tendril::new(),
577 }),
606 }),
578 }]
607 }]
579 }),
608 }),
580− parse("${TM_FILENAME/(.*).+$/$1/}")
609+ parse("${TM_FILENAME/(.*).+$/$1$/}")
610+ );
611+ }
612+
613+ #[test]
614+ fn rust_macro() {
615+ assert_eq!(
616+ Ok(Snippet {
617+ elements: vec![
618+ Text("macro_rules! ".into()),
619+ Tabstop { tabstop: 1 },
620+ Text(" {\n (".into()),
621+ Tabstop { tabstop: 2 },
622+ Text(") => {\n ".into()),
623+ Tabstop { tabstop: 0 },
624+ Text("\n };\n}".into())
625+ ]
626+ }),
627+ parse("macro_rules! $1 {\n ($2) => {\n $0\n };\n}")
628+ );
629+ }
630+ #[test]
631+ fn robust_parsing() {
632+ assert_eq!(
633+ Ok(Snippet {
634+ elements: vec![
635+ Text("$".into()),
636+ Text("{}".into()),
637+ Text("$".into()),
638+ Text("\\a$}\\".into()),
639+ ]
640+ }),
641+ parse("${}$\\a\\$\\}\\\\")
642+ );
643+ assert_eq!(
644+ Ok(Snippet {
645+ elements: vec![
646+ Placeholder {
647+ tabstop: 1,
648+ value: vec![Text("$".into()), Text("{".into())]
649+ },
650+ Text("}".into())
651+ ]
652+ }),
653+ parse("${1:${}}")
581 );
654 );
582 }
655 }
583 }
656 }