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.rs1010
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
- }
-}