diff options
Diffstat (limited to 'helix-lsp/src/snippet.rs')
-rw-r--r-- | helix-lsp/src/snippet.rs | 389 |
1 files changed, 368 insertions, 21 deletions
diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs index a11ff60f..a4f049e8 100644 --- a/helix-lsp/src/snippet.rs +++ b/helix-lsp/src/snippet.rs @@ -633,31 +633,378 @@ mod parser { parse("macro_rules! $1 {\n ($2) => {\n $0\n };\n}") ); } + + fn assert_text(snippet: &str, parsed_text: &str) { + let res = parse(snippet).unwrap(); + let text = crate::snippet::render(&res, "\n", true).0; + assert_eq!(text, parsed_text) + } + #[test] fn robust_parsing() { - assert_eq!( - Ok(Snippet { - elements: vec![ - Text("$".into()), - Text("{}".into()), - Text("$".into()), - Text("\\a$}\\".into()), - ] - }), - parse("${}$\\a\\$\\}\\\\") + assert_text("$", "$"); + assert_text("\\\\$", "\\$"); + assert_text("{", "{"); + assert_text("\\}", "}"); + assert_text("\\abc", "\\abc"); + assert_text("foo${f:\\}}bar", "foo}bar"); + assert_text("\\{", "\\{"); + assert_text("I need \\\\\\$", "I need \\$"); + assert_text("\\", "\\"); + assert_text("\\{{", "\\{{"); + assert_text("{{", "{{"); + assert_text("{{dd", "{{dd"); + assert_text("}}", "}}"); + assert_text("ff}}", "ff}}"); + assert_text("farboo", "farboo"); + assert_text("far{{}}boo", "far{{}}boo"); + assert_text("far{{123}}boo", "far{{123}}boo"); + assert_text("far\\{{123}}boo", "far\\{{123}}boo"); + assert_text("far{{id:bern}}boo", "far{{id:bern}}boo"); + assert_text("far{{id:bern {{basel}}}}boo", "far{{id:bern {{basel}}}}boo"); + assert_text( + "far{{id:bern {{id:basel}}}}boo", + "far{{id:bern {{id:basel}}}}boo", ); - assert_eq!( - Ok(Snippet { - elements: vec![ - Placeholder { - tabstop: 1, - value: vec![Text("$".into()), Text("{".into())] - }, - Text("}".into()) - ] - }), - parse("${1:${}}") + assert_text( + "far{{id:bern {{id2:basel}}}}boo", + "far{{id:bern {{id2:basel}}}}boo", + ); + assert_text("${}$\\a\\$\\}\\\\", "${}$\\a$}\\"); + assert_text("farboo", "farboo"); + assert_text("far{{}}boo", "far{{}}boo"); + assert_text("far{{123}}boo", "far{{123}}boo"); + assert_text("far\\{{123}}boo", "far\\{{123}}boo"); + assert_text("far`123`boo", "far`123`boo"); + assert_text("far\\`123\\`boo", "far\\`123\\`boo"); + assert_text("\\$far-boo", "$far-boo"); + } + + fn assert_snippet(snippet: &str, expect: &[SnippetElement]) { + let parsed_snippet = parse(snippet).unwrap(); + assert_eq!(parsed_snippet.elements, expect.to_owned()) + } + + #[test] + fn parse_variable() { + use SnippetElement::*; + assert_snippet( + "$far-boo", + &[ + Variable { + name: "far", + default: None, + regex: None, + }, + Text("-boo".into()), + ], + ); + assert_snippet( + "far$farboo", + &[ + Text("far".into()), + Variable { + name: "farboo", + regex: None, + default: None, + }, + ], + ); + assert_snippet( + "far${farboo}", + &[ + Text("far".into()), + Variable { + name: "farboo", + regex: None, + default: None, + }, + ], + ); + assert_snippet("$123", &[Tabstop { tabstop: 123 }]); + assert_snippet( + "$farboo", + &[Variable { + name: "farboo", + regex: None, + default: None, + }], + ); + assert_snippet( + "$far12boo", + &[Variable { + name: "far12boo", + regex: None, + default: None, + }], + ); + assert_snippet( + "000_${far}_000", + &[ + Text("000_".into()), + Variable { + name: "far", + regex: None, + default: None, + }, + Text("_000".into()), + ], + ); + } + + #[test] + fn parse_variable_transform() { + assert_snippet( + "${foo///}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: Tendril::new(), + replacement: Vec::new(), + options: Tendril::new(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/regex/format/gmi}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: "regex".into(), + replacement: vec![FormatItem::Text("format".into())], + options: "gmi".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/([A-Z][a-z])/format/}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: "([A-Z][a-z])".into(), + replacement: vec![FormatItem::Text("format".into())], + options: Tendril::new(), + }), + default: None, + }], + ); + + // invalid regex TODO: reneable tests once we actually parse this regex flavour + // assert_text( + // "${foo/([A-Z][a-z])/format/GMI}", + // "${foo/([A-Z][a-z])/format/GMI}", + // ); + // assert_text( + // "${foo/([A-Z][a-z])/format/funky}", + // "${foo/([A-Z][a-z])/format/funky}", + // ); + // assert_text("${foo/([A-Z][a-z]/format/}", "${foo/([A-Z][a-z]/format/}"); + assert_text( + "${foo/regex\\/format/options}", + "${foo/regex\\/format/options}", + ); + + // tricky regex + assert_snippet( + "${foo/m\\/atch/$1/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: "m/atch".into(), + replacement: vec![FormatItem::Capture(1)], + options: "i".into(), + }), + default: None, + }], + ); + + // incomplete + assert_text("${foo///", "${foo///"); + assert_text("${foo/regex/format/options", "${foo/regex/format/options"); + + // format string + assert_snippet( + "${foo/.*/${0:fooo}/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![FormatItem::Conditional(0, None, Some("fooo".into()))], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/.*/${1}/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![FormatItem::Capture(1)], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/.*/$1/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![FormatItem::Capture(1)], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/.*/This-$1-encloses/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![ + FormatItem::Text("This-".into()), + FormatItem::Capture(1), + FormatItem::Text("-encloses".into()), + ], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/.*/complex${1:else}/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![ + FormatItem::Text("complex".into()), + FormatItem::Conditional(1, None, Some("else".into())), + ], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/.*/complex${1:-else}/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![ + FormatItem::Text("complex".into()), + FormatItem::Conditional(1, None, Some("else".into())), + ], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/.*/complex${1:+if}/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![ + FormatItem::Text("complex".into()), + FormatItem::Conditional(1, Some("if".into()), None), + ], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/.*/complex${1:?if:else}/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![ + FormatItem::Text("complex".into()), + FormatItem::Conditional(1, Some("if".into()), Some("else".into())), + ], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${foo/.*/complex${1:/upcase}/i}", + &[Variable { + name: "foo", + regex: Some(Regex { + value: ".*".into(), + replacement: vec![ + FormatItem::Text("complex".into()), + FormatItem::CaseChange(1, CaseChange::Upcase), + ], + options: "i".into(), + }), + default: None, + }], + ); + assert_snippet( + "${TM_DIRECTORY/src\\//$1/}", + &[Variable { + name: "TM_DIRECTORY", + regex: Some(Regex { + value: "src/".into(), + replacement: vec![FormatItem::Capture(1)], + options: Tendril::new(), + }), + default: None, + }], + ); + assert_snippet( + "${TM_SELECTED_TEXT/a/\\/$1/g}", + &[Variable { + name: "TM_SELECTED_TEXT", + regex: Some(Regex { + value: "a".into(), + replacement: vec![FormatItem::Text("/".into()), FormatItem::Capture(1)], + options: "g".into(), + }), + default: None, + }], + ); + assert_snippet( + "${TM_SELECTED_TEXT/a/in\\/$1ner/g}", + &[Variable { + name: "TM_SELECTED_TEXT", + regex: Some(Regex { + value: "a".into(), + replacement: vec![ + FormatItem::Text("in/".into()), + FormatItem::Capture(1), + FormatItem::Text("ner".into()), + ], + options: "g".into(), + }), + default: None, + }], + ); + assert_snippet( + "${TM_SELECTED_TEXT/a/end\\//g}", + &[Variable { + name: "TM_SELECTED_TEXT", + regex: Some(Regex { + value: "a".into(), + replacement: vec![FormatItem::Text("end/".into())], + options: "g".into(), + }), + default: None, + }], ); } + // TODO port more tests from https://github.com/microsoft/vscode/blob/dce493cb6e36346ef2714e82c42ce14fc461b15c/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts } } |