Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-lsp/src/snippet.rs')
| -rw-r--r-- | helix-lsp/src/snippet.rs | 1010 |
1 files changed, 0 insertions, 1010 deletions
diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs deleted file mode 100644 index ebf3da24..00000000 --- a/helix-lsp/src/snippet.rs +++ /dev/null @@ -1,1010 +0,0 @@ -use std::borrow::Cow; - -use anyhow::{anyhow, Result}; -use helix_core::{smallvec, SmallVec, Tendril}; - -#[derive(Debug, PartialEq, Eq)] -pub enum CaseChange { - Upcase, - Downcase, - Capitalize, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum FormatItem { - Text(Tendril), - Capture(usize), - CaseChange(usize, CaseChange), - Conditional(usize, Option<Tendril>, Option<Tendril>), -} - -#[derive(Debug, PartialEq, Eq)] -pub struct Regex { - value: Tendril, - replacement: Vec<FormatItem>, - options: Tendril, -} - -#[derive(Debug, PartialEq, Eq)] -pub enum SnippetElement<'a> { - Tabstop { - tabstop: usize, - }, - Placeholder { - tabstop: usize, - value: Vec<SnippetElement<'a>>, - }, - Choice { - tabstop: usize, - choices: Vec<Tendril>, - }, - Variable { - name: &'a str, - default: Option<Vec<SnippetElement<'a>>>, - regex: Option<Regex>, - }, - Text(Tendril), -} - -#[derive(Debug, PartialEq, Eq)] -pub struct Snippet<'a> { - elements: Vec<SnippetElement<'a>>, -} - -pub fn parse(s: &str) -> Result<Snippet<'_>> { - parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest)) -} - -fn render_elements( - snippet_elements: &[SnippetElement<'_>], - insert: &mut Tendril, - offset: &mut usize, - tabstops: &mut Vec<(usize, (usize, usize))>, - newline_with_offset: &str, - include_placeholder: bool, -) { - use SnippetElement::*; - - for element in snippet_elements { - match element { - Text(text) => { - // small optimization to avoid calling replace when it's unnecessary - let text = if text.contains('\n') { - Cow::Owned(text.replace('\n', newline_with_offset)) - } else { - Cow::Borrowed(text.as_str()) - }; - *offset += text.chars().count(); - insert.push_str(&text); - } - Variable { - name: _, - regex: _, - r#default, - } => { - // TODO: variables. For now, fall back to the default, which defaults to "". - render_elements( - r#default.as_deref().unwrap_or_default(), - insert, - offset, - tabstops, - newline_with_offset, - include_placeholder, - ); - } - &Tabstop { tabstop } => { - tabstops.push((tabstop, (*offset, *offset))); - } - Placeholder { - tabstop, - value: inner_snippet_elements, - } => { - let start_offset = *offset; - if include_placeholder { - render_elements( - inner_snippet_elements, - insert, - offset, - tabstops, - newline_with_offset, - include_placeholder, - ); - } - tabstops.push((*tabstop, (start_offset, *offset))); - } - &Choice { - tabstop, - choices: _, - } => { - // TODO: choices - tabstops.push((tabstop, (*offset, *offset))); - } - } - } -} - -#[allow(clippy::type_complexity)] // only used one time -pub fn render( - snippet: &Snippet<'_>, - newline_with_offset: &str, - include_placeholder: bool, -) -> (Tendril, Vec<SmallVec<[(usize, usize); 1]>>) { - let mut insert = Tendril::new(); - let mut tabstops = Vec::new(); - let mut offset = 0; - - render_elements( - &snippet.elements, - &mut insert, - &mut offset, - &mut tabstops, - newline_with_offset, - include_placeholder, - ); - - // sort in ascending order (except for 0, which should always be the last one (per lsp doc)) - tabstops.sort_unstable_by_key(|(n, _)| if *n == 0 { usize::MAX } else { *n }); - - // merge tabstops with the same index (we take advantage of the fact that we just sorted them - // above to simply look backwards) - let mut ntabstops = Vec::<SmallVec<[(usize, usize); 1]>>::new(); - { - let mut prev = None; - for (tabstop, r) in tabstops { - if prev == Some(tabstop) { - let len_1 = ntabstops.len() - 1; - ntabstops[len_1].push(r); - } else { - prev = Some(tabstop); - ntabstops.push(smallvec![r]); - } - } - } - - (insert, ntabstops) -} - -mod parser { - use helix_core::Tendril; - use helix_parsec::*; - - use super::{CaseChange, FormatItem, Regex, Snippet, SnippetElement}; - - /* - https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax - - any ::= tabstop | placeholder | choice | variable | text - tabstop ::= '$' int | '${' int '}' - placeholder ::= '${' int ':' any '}' - choice ::= '${' int '|' text (',' text)* '|}' - variable ::= '$' var | '${' var }' - | '${' var ':' any '}' - | '${' var '/' regex '/' (format | text)+ '/' options '}' - format ::= '$' int | '${' int '}' - | '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' - | '${' int ':+' if '}' - | '${' int ':?' if ':' else '}' - | '${' int ':-' else '}' | '${' int ':' else '}' - regex ::= Regular Expression value (ctor-string) - options ::= Regular Expression option (ctor-options) - var ::= [_a-zA-Z] [_a-zA-Z0-9]* - int ::= [0-9]+ - text ::= .* - if ::= text - else ::= text - */ - - fn var<'a>() -> impl Parser<'a, Output = &'a str> { - // var = [_a-zA-Z][_a-zA-Z0-9]* - move |input: &'a str| { - input - .char_indices() - .take_while(|(p, c)| { - *c == '_' - || if *p == 0 { - c.is_ascii_alphabetic() - } else { - c.is_ascii_alphanumeric() - } - }) - .last() - .map(|(index, c)| { - let index = index + c.len_utf8(); - (&input[index..], &input[0..index]) - }) - .ok_or(input) - } - } - - const TEXT_ESCAPE_CHARS: &[char] = &['\\', '}', '$']; - const CHOICE_TEXT_ESCAPE_CHARS: &[char] = &['\\', '|', ',']; - - fn text<'a>( - escape_chars: &'static [char], - term_chars: &'static [char], - ) -> impl Parser<'a, Output = Tendril> { - move |input: &'a str| { - let mut chars = input.char_indices().peekable(); - let mut res = Tendril::new(); - while let Some((i, c)) = chars.next() { - match c { - '\\' => { - if let Some(&(_, c)) = chars.peek() { - if escape_chars.contains(&c) { - chars.next(); - res.push(c); - continue; - } - } - res.push('\\'); - } - c if term_chars.contains(&c) => return Ok((&input[i..], res)), - c => res.push(c), - } - } - - Ok(("", res)) - } - } - - fn digit<'a>() -> impl Parser<'a, Output = usize> { - filter_map(take_while(|c| c.is_ascii_digit()), |s| s.parse().ok()) - } - - fn case_change<'a>() -> impl Parser<'a, Output = CaseChange> { - use CaseChange::*; - - choice!( - map("upcase", |_| Upcase), - map("downcase", |_| Downcase), - map("capitalize", |_| Capitalize), - ) - } - - fn format<'a>() -> impl Parser<'a, Output = FormatItem> { - use FormatItem::*; - - choice!( - // '$' int - map(right("$", digit()), Capture), - // '${' int '}' - map(seq!("${", digit(), "}"), |seq| Capture(seq.1)), - // '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' - map(seq!("${", digit(), ":/", case_change(), "}"), |seq| { - CaseChange(seq.1, seq.3) - }), - // '${' int ':+' if '}' - map( - seq!("${", digit(), ":+", text(TEXT_ESCAPE_CHARS, &['}']), "}"), - |seq| { Conditional(seq.1, Some(seq.3), None) } - ), - // '${' int ':?' if ':' else '}' - map( - seq!( - "${", - digit(), - ":?", - text(TEXT_ESCAPE_CHARS, &[':']), - ":", - text(TEXT_ESCAPE_CHARS, &['}']), - "}" - ), - |seq| { Conditional(seq.1, Some(seq.3), Some(seq.5)) } - ), - // '${' int ':-' else '}' | '${' int ':' else '}' - map( - seq!( - "${", - digit(), - ":", - optional("-"), - text(TEXT_ESCAPE_CHARS, &['}']), - "}" - ), - |seq| { Conditional(seq.1, None, Some(seq.4)) } - ), - ) - } - - fn regex<'a>() -> impl Parser<'a, Output = Regex> { - map( - seq!( - "/", - // TODO parse as ECMAScript and convert to rust regex - text(&['/'], &['/']), - "/", - zero_or_more(choice!( - format(), - // text doesn't parse $, if format fails we just accept the $ as text - map("$", |_| FormatItem::Text("$".into())), - map(text(&['\\', '/'], &['/', '$']), FormatItem::Text), - )), - "/", - // vscode really doesn't allow escaping } here - // so it's impossible to write a regex escape containing a } - // we can consider deviating here and allowing the escape - text(&[], &['}']), - ), - |(_, value, _, replacement, _, options)| Regex { - value, - replacement, - options, - }, - ) - } - - fn tabstop<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { - map( - or( - right("$", digit()), - map(seq!("${", digit(), "}"), |values| values.1), - ), - |digit| SnippetElement::Tabstop { tabstop: digit }, - ) - } - - fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { - map( - seq!( - "${", - digit(), - ":", - // according to the grammar there is just a single anything here. - // However in the prose it is explained that placeholders can be nested. - // The example there contains both a placeholder text and a nested placeholder - // which indicates a list. Looking at the VSCode sourcecode, the placeholder - // is indeed parsed as zero_or_more so the grammar is simply incorrect here - zero_or_more(anything(TEXT_ESCAPE_CHARS, true)), - "}" - ), - |seq| SnippetElement::Placeholder { - tabstop: seq.1, - value: seq.3, - }, - ) - } - - fn choice<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { - map( - seq!( - "${", - digit(), - "|", - sep(text(CHOICE_TEXT_ESCAPE_CHARS, &['|', ',']), ","), - "|}", - ), - |seq| SnippetElement::Choice { - tabstop: seq.1, - choices: seq.3, - }, - ) - } - - fn variable<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { - choice!( - // $var - map(right("$", var()), |name| SnippetElement::Variable { - name, - default: None, - regex: None, - }), - // ${var} - map(seq!("${", var(), "}",), |values| SnippetElement::Variable { - name: values.1, - default: None, - regex: None, - }), - // ${var:default} - map( - seq!( - "${", - var(), - ":", - zero_or_more(anything(TEXT_ESCAPE_CHARS, true)), - "}", - ), - |values| SnippetElement::Variable { - name: values.1, - default: Some(values.3), - regex: None, - } - ), - // ${var/value/format/options} - map(seq!("${", var(), regex(), "}"), |values| { - SnippetElement::Variable { - name: values.1, - default: None, - regex: Some(values.2), - } - }), - ) - } - - fn anything<'a>( - escape_chars: &'static [char], - end_at_brace: bool, - ) -> impl Parser<'a, Output = SnippetElement<'a>> { - let term_chars: &[_] = if end_at_brace { &['$', '}'] } else { &['$'] }; - move |input: &'a str| { - let parser = choice!( - tabstop(), - placeholder(), - choice(), - variable(), - map("$", |_| SnippetElement::Text("$".into())), - map(text(escape_chars, term_chars), SnippetElement::Text), - ); - parser.parse(input) - } - } - - fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> { - map(one_or_more(anything(TEXT_ESCAPE_CHARS, false)), |parts| { - Snippet { elements: parts } - }) - } - - pub fn parse(s: &str) -> Result<Snippet, &str> { - snippet().parse(s).and_then(|(remainder, snippet)| { - if remainder.is_empty() { - Ok(snippet) - } else { - Err(remainder) - } - }) - } - - #[cfg(test)] - mod test { - use super::SnippetElement::*; - use super::*; - - #[test] - fn empty_string_is_error() { - assert_eq!(Err(""), parse("")); - } - - #[test] - fn parse_placeholders_in_function_call() { - assert_eq!( - Ok(Snippet { - elements: vec![ - Text("match(".into()), - Placeholder { - tabstop: 1, - value: vec!(Text("Arg1".into())), - }, - Text(")".into()) - ] - }), - parse("match(${1:Arg1})") - ) - } - - #[test] - fn unterminated_placeholder() { - assert_eq!( - Ok(Snippet { - elements: vec![Text("match(".into()), Text("$".into()), Text("{1:)".into())] - }), - parse("match(${1:)") - ) - } - - #[test] - fn parse_empty_placeholder() { - assert_eq!( - Ok(Snippet { - elements: vec![ - Text("match(".into()), - Placeholder { - tabstop: 1, - value: vec![], - }, - Text(")".into()) - ] - }), - parse("match(${1:})") - ) - } - - #[test] - fn parse_placeholders_in_statement() { - assert_eq!( - Ok(Snippet { - elements: vec![ - Text("local ".into()), - Placeholder { - tabstop: 1, - value: vec!(Text("var".into())), - }, - Text(" = ".into()), - Placeholder { - tabstop: 1, - value: vec!(Text("value".into())), - }, - ] - }), - parse("local ${1:var} = ${1:value}") - ) - } - - #[test] - fn parse_tabstop_nested_in_placeholder() { - assert_eq!( - Ok(Snippet { - elements: vec![Placeholder { - tabstop: 1, - value: vec!(Text("var, ".into()), Tabstop { tabstop: 2 },), - },] - }), - parse("${1:var, $2}") - ) - } - - #[test] - fn parse_placeholder_nested_in_placeholder() { - assert_eq!( - Ok(Snippet { - elements: vec![Placeholder { - tabstop: 1, - value: vec!( - Text("foo ".into()), - Placeholder { - tabstop: 2, - value: vec!(Text("bar".into())), - }, - ), - },] - }), - parse("${1:foo ${2:bar}}") - ) - } - - #[test] - fn parse_all() { - assert_eq!( - Ok(Snippet { - elements: vec![ - Text("hello ".into()), - Tabstop { tabstop: 1 }, - Tabstop { tabstop: 2 }, - Text(" ".into()), - Choice { - tabstop: 1, - choices: vec!["one".into(), "two".into(), "three".into()] - }, - Text(" ".into()), - Variable { - name: "name", - default: Some(vec![Text("foo".into())]), - regex: None - }, - Text(" ".into()), - Variable { - name: "var", - default: None, - regex: None - }, - Text(" ".into()), - Variable { - name: "TM", - default: None, - regex: None - }, - ] - }), - parse("hello $1${2} ${1|one,two,three|} ${name:foo} $var $TM") - ); - } - - #[test] - fn regex_capture_replace() { - assert_eq!( - Ok(Snippet { - elements: vec![Variable { - name: "TM_FILENAME", - default: None, - regex: Some(Regex { - value: "(.*).+$".into(), - replacement: vec![FormatItem::Capture(1), FormatItem::Text("$".into())], - options: Tendril::new(), - }), - }] - }), - parse("${TM_FILENAME/(.*).+$/$1$/}") - ); - } - - #[test] - fn rust_macro() { - assert_eq!( - Ok(Snippet { - elements: vec![ - Text("macro_rules! ".into()), - Tabstop { tabstop: 1 }, - Text(" {\n (".into()), - Tabstop { tabstop: 2 }, - Text(") => {\n ".into()), - Tabstop { tabstop: 0 }, - Text("\n };\n}".into()) - ] - }), - 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_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_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 - } -} |