@ -11,18 +11,18 @@ pub enum CaseChange {
}
#[ derive(Debug, PartialEq, Eq) ]
pub enum FormatItem < ' a > {
Text ( & ' a str ) ,
pub enum FormatItem {
Text ( Tendril ) ,
Capture ( usize ) ,
CaseChange ( usize , CaseChange ) ,
Conditional ( usize , Option < & ' a str > , Option < & ' a str > ) ,
Conditional ( usize , Option < Tendril > , Option < Tendril > ) ,
}
#[ derive(Debug, PartialEq, Eq) ]
pub struct Regex < ' a > {
value : & ' a str ,
replacement : Vec < FormatItem <' a > >,
options : Option < & ' a str > ,
pub struct Regex {
value : Tendril ,
replacement : Vec < FormatItem >,
options : Tendril ,
}
#[ derive(Debug, PartialEq, Eq) ]
@ -36,14 +36,14 @@ pub enum SnippetElement<'a> {
} ,
Choice {
tabstop : usize ,
choices : Vec < & ' a str > ,
choices : Vec < Tendril > ,
} ,
Variable {
name : & ' a str ,
default : Option < & ' a str > ,
regex : Option < Regex <' a > >,
default : Option < Vec < SnippetElement < ' a > > > ,
regex : Option < Regex >,
} ,
Text ( & ' a str ) ,
Text ( Tendril ) ,
}
#[ derive(Debug, PartialEq, Eq) ]
@ -67,25 +67,30 @@ fn render_elements(
for element in snippet_elements {
match element {
& Text ( text ) = > {
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 )
Cow ::Borrowed ( text .as_str ( ) )
} ;
* offset + = text . chars ( ) . count ( ) ;
insert . push_str ( & text ) ;
}
& Variable {
Variable {
name : _ ,
regex : _ ,
r#default ,
} = > {
// TODO: variables. For now, fall back to the default, which defaults to "".
let text = r#default . unwrap_or_default ( ) ;
* offset + = text . chars ( ) . count ( ) ;
insert . push_str ( text ) ;
render_elements (
r#default . as_deref ( ) . unwrap_or_default ( ) ,
insert ,
offset ,
tabstops ,
newline_with_offset ,
include_placeholer ,
) ;
}
& Tabstop { tabstop } = > {
tabstops . push ( ( tabstop , ( * offset , * offset ) ) ) ;
@ -160,6 +165,7 @@ pub fn render(
}
mod parser {
use helix_core ::Tendril ;
use helix_parsec ::* ;
use super ::{ CaseChange , FormatItem , Regex , Snippet , SnippetElement } ;
@ -190,28 +196,55 @@ mod parser {
fn var < ' a > ( ) -> impl Parser < ' a , Output = & ' a str > {
// var = [_a-zA-Z][_a-zA-Z0-9]*
move | input : & ' a str | match input
. char_indices ( )
. take_while ( | ( p , c ) | {
* c = = '_'
|| if * p = = 0 {
c . is_ascii_alphabetic ( )
} else {
c . is_ascii_alphanumeric ( )
}
} )
. last ( )
{
Some ( ( index , c ) ) if index > = 1 = > {
let index = index + c . len_utf8 ( ) ;
Ok ( ( & input [ index .. ] , & input [ 0 .. index ] ) )
}
_ = > Err ( input ) ,
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 )
}
}
fn text < ' a , const SIZE : usize > ( cs : [ char ; SIZE ] ) -> impl Parser < ' a , Output = & ' a str > {
take_while ( move | c | cs . into_iter ( ) . all ( | c1 | c ! = c1 ) )
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 > {
@ -228,7 +261,7 @@ mod parser {
)
}
fn format < ' a > ( ) -> impl Parser < ' a , Output = FormatItem <' a > > {
fn format < ' a > ( ) -> impl Parser < ' a , Output = FormatItem > {
use FormatItem ::* ;
choice ! (
@ -242,7 +275,7 @@ mod parser {
} ) ,
// '${' int ':+' if '}'
map (
seq ! ( "${" , digit ( ) , ":+" , t ake_until( | c | c = = '}' ) , "}" ) ,
seq ! ( "${" , digit ( ) , ":+" , t ext( TEXT_ESCAPE_CHARS , & [ '}' ] ) , "}" ) ,
| seq | { Conditional ( seq . 1 , Some ( seq . 3 ) , None ) }
) ,
// '${' int ':?' if ':' else '}'
@ -251,9 +284,9 @@ mod parser {
"${" ,
digit ( ) ,
":?" ,
t ake_until( | c | c = = ':' ) ,
t ext( TEXT_ESCAPE_CHARS , & [ ':' ] ) ,
":" ,
t ake_until( | c | c = = '}' ) ,
t ext( TEXT_ESCAPE_CHARS , & [ '}' ] ) ,
"}"
) ,
| seq | { Conditional ( seq . 1 , Some ( seq . 3 ) , Some ( seq . 5 ) ) }
@ -265,7 +298,7 @@ mod parser {
digit ( ) ,
":" ,
optional ( "-" ) ,
t ake_until( | c | c = = '}' ) ,
t ext( TEXT_ESCAPE_CHARS , & [ '}' ] ) ,
"}"
) ,
| seq | { Conditional ( seq . 1 , None , Some ( seq . 4 ) ) }
@ -273,21 +306,24 @@ mod parser {
)
}
fn regex < ' a > ( ) -> impl Parser < ' a , Output = Regex < ' a > > {
let text = map ( text ( [ '$' , '/' ] ) , FormatItem ::Text ) ;
let replacement = reparse_as (
take_until ( | c | c = = '/' ) ,
one_or_more ( choice ! ( format ( ) , text ) ) ,
) ;
fn regex < ' a > ( ) -> impl Parser < ' a , Output = Regex > {
map (
seq ! (
"/" ,
take_until ( | c | c = = '/' ) ,
// TODO parse as ECMAScript and convert to rust regex
text ( & [ '/' ] , & [ '/' ] ) ,
"/" ,
replacement ,
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 ) ,
) ) ,
"/" ,
optional ( take_until ( | c | c = = '}' ) ) ,
// 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 ,
@ -308,13 +344,17 @@ mod parser {
}
fn placeholder < ' a > ( ) -> impl Parser < ' a , Output = SnippetElement < ' a > > {
let text = map ( text ( [ '$' , '}' ] ) , SnippetElement ::Text ) ;
map (
seq ! (
"${" ,
digit ( ) ,
":" ,
one_or_more ( choice ! ( anything ( ) , text ) ) ,
// 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 {
@ -330,7 +370,7 @@ mod parser {
"${" ,
digit ( ) ,
"|" ,
sep ( t ake_until( | c | c = = ',' | | c = = '|' ) , "," ) ,
sep ( t ext( CHOICE_TEXT_ESCAPE_CHARS , & [ '|' , ',' ] ) , "," ) ,
"|}" ,
) ,
| seq | SnippetElement ::Choice {
@ -348,9 +388,21 @@ mod parser {
default : None ,
regex : None ,
} ) ,
// ${var}
map ( seq ! ( "${" , var ( ) , "}" , ) , | values | SnippetElement ::Variable {
name : values . 1 ,
default : None ,
regex : None ,
} ) ,
// ${var:default}
map (
seq ! ( "${" , var ( ) , ":" , take_until ( | c | c = = '}' ) , "}" , ) ,
seq ! (
"${" ,
var ( ) ,
":" ,
zero_or_more ( anything ( TEXT_ESCAPE_CHARS , true ) ) ,
"}" ,
) ,
| values | SnippetElement ::Variable {
name : values . 1 ,
default : Some ( values . 3 ) ,
@ -368,23 +420,38 @@ mod parser {
)
}
fn anything < ' a > ( ) -> impl Parser < ' a , Output = SnippetElement < ' a > > {
// The parser has to be constructed lazily to avoid infinite opaque type recursion
| input : & ' a str | {
let parser = choice ! ( tabstop ( ) , placeholder ( ) , choice ( ) , variable ( ) ) ;
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 > > {
let text = map ( text ( [ '$' ] ) , SnippetElement ::Text ) ;
map ( one_or_more ( choice ! ( anything ( ) , text ) ) , | parts | Snippet {
elements : parts ,
map ( one_or_more ( anything ( TEXT_ESCAPE_CHARS , false ) ) , | parts | {
Snippet { elements : parts }
} )
}
pub fn parse ( s : & str ) -> Result < Snippet , & str > {
snippet ( ) . parse ( s ) . map ( | ( _input , elements ) | elements )
snippet ( ) . parse ( s ) . and_then ( | ( remainder , snippet ) | {
if remainder . is_empty ( ) {
Ok ( snippet )
} else {
Err ( remainder )
}
} )
}
#[ cfg(test) ]
@ -402,32 +469,59 @@ mod parser {
assert_eq! (
Ok ( Snippet {
elements : vec ! [
Text ( "match(" ),
Text ( "match(" .into ( ) ),
Placeholder {
tabstop : 1 ,
value : vec ! ( Text ( "Arg1" )) ,
value : vec ! ( Text ( "Arg1" .into ( ) )) ,
} ,
Text ( ")" )
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 " ) ,
Text ( "local " .into ( ) ),
Placeholder {
tabstop : 1 ,
value : vec ! ( Text ( "var" ) ) ,
value : vec ! ( Text ( "var" .into ( ) )) ,
} ,
Text ( " = " ) ,
Text ( " = " .into ( ) ),
Placeholder {
tabstop : 1 ,
value : vec ! ( Text ( "value" ) ) ,
value : vec ! ( Text ( "value" .into ( ) )) ,
} ,
]
} ) ,
@ -441,7 +535,7 @@ mod parser {
Ok ( Snippet {
elements : vec ! [ Placeholder {
tabstop : 1 ,
value : vec ! ( Text ( "var, " ), Tabstop { tabstop : 2 } , ) ,
value : vec ! ( Text ( "var, " .into ( ) ), Tabstop { tabstop : 2 } , ) ,
} , ]
} ) ,
parse ( "${1:var, $2}" )
@ -455,10 +549,10 @@ mod parser {
elements : vec ! [ Placeholder {
tabstop : 1 ,
value : vec ! (
Text ( "foo " ),
Text ( "foo " .into ( ) ),
Placeholder {
tabstop : 2 ,
value : vec ! ( Text ( "bar" )) ,
value : vec ! ( Text ( "bar" .into ( ) )) ,
} ,
) ,
} , ]
@ -472,27 +566,27 @@ mod parser {
assert_eq! (
Ok ( Snippet {
elements : vec ! [
Text ( "hello " ),
Text ( "hello " .into ( ) ),
Tabstop { tabstop : 1 } ,
Tabstop { tabstop : 2 } ,
Text ( " " ),
Text ( " " .into ( ) ),
Choice {
tabstop : 1 ,
choices : vec ! [ "one" , "two" , "three" ]
choices : vec ! [ "one" .into ( ) , "two" .into ( ) , "three" . into ( ) ]
} ,
Text ( " " ),
Text ( " " .into ( ) ),
Variable {
name : "name" ,
default : Some ( "foo" ) ,
default : Some ( vec! [ Text ( "foo" . into ( ) ) ] ) ,
regex : None
} ,
Text ( " " ),
Text ( " " .into ( ) ),
Variable {
name : "var" ,
default : None ,
regex : None
} ,
Text ( " " ),
Text ( " " .into ( ) ),
Variable {
name : "TM" ,
default : None ,
@ -512,14 +606,405 @@ mod parser {
name : "TM_FILENAME" ,
default : None ,
regex : Some ( Regex {
value : "(.*).+$" ,
replacement : vec ! [ FormatItem ::Capture ( 1 ) ],
options : None ,
value : "(.*).+$" .into ( ) ,
replacement : vec ! [ FormatItem ::Capture ( 1 ) , FormatItem ::Text ( "$" . into ( ) ) ],
options : Tendril ::new ( ) ,
} ) ,
} ]
} ) ,
parse ( "${TM_FILENAME/(.*).+$/$1/}" )
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
}
}