2022-10-22 22:52:25 +08:00
use std ::borrow ::Cow ;
2022-10-04 09:41:31 +08:00
use anyhow ::{ anyhow , Result } ;
2023-03-10 05:17:12 +08:00
use helix_core ::{ smallvec , SmallVec , Tendril } ;
2022-10-04 09:41:31 +08:00
#[ derive(Debug, PartialEq, Eq) ]
pub enum CaseChange {
Upcase ,
Downcase ,
Capitalize ,
}
#[ derive(Debug, PartialEq, Eq) ]
2023-03-20 03:32:46 +08:00
pub enum FormatItem {
2023-03-14 02:27:54 +08:00
Text ( Tendril ) ,
2022-10-04 09:41:31 +08:00
Capture ( usize ) ,
CaseChange ( usize , CaseChange ) ,
2023-03-20 03:32:46 +08:00
Conditional ( usize , Option < Tendril > , Option < Tendril > ) ,
2022-10-04 09:41:31 +08:00
}
#[ derive(Debug, PartialEq, Eq) ]
2023-03-20 03:32:46 +08:00
pub struct Regex {
2023-03-14 02:27:54 +08:00
value : Tendril ,
2023-03-20 03:32:46 +08:00
replacement : Vec < FormatItem > ,
2023-03-14 02:27:54 +08:00
options : Tendril ,
2022-10-04 09:41:31 +08:00
}
#[ derive(Debug, PartialEq, Eq) ]
pub enum SnippetElement < ' a > {
Tabstop {
tabstop : usize ,
} ,
Placeholder {
tabstop : usize ,
2023-02-17 23:51:00 +08:00
value : Vec < SnippetElement < ' a > > ,
2022-10-04 09:41:31 +08:00
} ,
Choice {
tabstop : usize ,
2023-03-14 02:27:54 +08:00
choices : Vec < Tendril > ,
2022-10-04 09:41:31 +08:00
} ,
Variable {
name : & ' a str ,
2023-03-20 03:32:46 +08:00
default : Option < Vec < SnippetElement < ' a > > > ,
regex : Option < Regex > ,
2022-10-04 09:41:31 +08:00
} ,
2023-03-14 02:27:54 +08:00
Text ( Tendril ) ,
2022-10-04 09:41:31 +08:00
}
#[ derive(Debug, PartialEq, Eq) ]
pub struct Snippet < ' a > {
elements : Vec < SnippetElement < ' a > > ,
}
2023-02-08 03:15:39 +08:00
pub fn parse ( s : & str ) -> Result < Snippet < '_ > > {
2022-10-04 09:41:31 +08:00
parser ::parse ( s ) . map_err ( | rest | anyhow! ( " Failed to parse snippet. Remaining input: {} " , rest ) )
}
2023-02-17 23:51:00 +08:00
fn render_elements (
snippet_elements : & [ SnippetElement < '_ > ] ,
2023-03-10 05:17:12 +08:00
insert : & mut Tendril ,
2023-02-17 23:51:00 +08:00
offset : & mut usize ,
tabstops : & mut Vec < ( usize , ( usize , usize ) ) > ,
2023-03-10 05:17:12 +08:00
newline_with_offset : & str ,
2023-02-11 21:20:49 +08:00
include_placeholer : bool ,
2023-02-17 23:51:00 +08:00
) {
2022-10-22 22:52:25 +08:00
use SnippetElement ::* ;
2023-02-17 23:51:00 +08:00
for element in snippet_elements {
2022-10-22 22:52:25 +08:00
match element {
2023-03-14 02:27:54 +08:00
Text ( text ) = > {
2022-10-22 22:52:25 +08:00
// small optimization to avoid calling replace when it's unnecessary
let text = if text . contains ( '\n' ) {
2023-02-17 23:51:00 +08:00
Cow ::Owned ( text . replace ( '\n' , newline_with_offset ) )
2022-10-22 22:52:25 +08:00
} else {
2023-03-14 02:27:54 +08:00
Cow ::Borrowed ( text . as_str ( ) )
2022-10-22 22:52:25 +08:00
} ;
2023-02-17 23:51:00 +08:00
* offset + = text . chars ( ) . count ( ) ;
2022-10-22 22:52:25 +08:00
insert . push_str ( & text ) ;
}
2023-03-20 03:32:46 +08:00
Variable {
2023-02-17 23:51:00 +08:00
name : _ ,
regex : _ ,
2022-10-22 22:52:25 +08:00
r#default ,
} = > {
// TODO: variables. For now, fall back to the default, which defaults to "".
2023-03-20 03:32:46 +08:00
render_elements (
r#default . as_deref ( ) . unwrap_or_default ( ) ,
insert ,
offset ,
tabstops ,
newline_with_offset ,
include_placeholer ,
) ;
2022-10-22 22:52:25 +08:00
}
2023-02-17 23:51:00 +08:00
& Tabstop { tabstop } = > {
tabstops . push ( ( tabstop , ( * offset , * offset ) ) ) ;
2022-10-22 22:52:25 +08:00
}
2023-02-17 23:51:00 +08:00
Placeholder {
tabstop ,
value : inner_snippet_elements ,
} = > {
let start_offset = * offset ;
if include_placeholer {
render_elements (
inner_snippet_elements ,
insert ,
offset ,
tabstops ,
newline_with_offset ,
include_placeholer ,
2022-10-22 22:52:25 +08:00
) ;
}
2023-02-17 23:51:00 +08:00
tabstops . push ( ( * tabstop , ( start_offset , * offset ) ) ) ;
}
& Choice {
tabstop ,
choices : _ ,
} = > {
// TODO: choices
tabstops . push ( ( tabstop , ( * offset , * offset ) ) ) ;
2022-10-22 22:52:25 +08:00
}
}
}
2023-02-17 23:51:00 +08:00
}
2022-10-22 22:52:25 +08:00
2023-02-17 23:51:00 +08:00
#[ allow(clippy::type_complexity) ] // only used one time
pub fn render (
snippet : & Snippet < '_ > ,
2023-03-10 05:17:12 +08:00
newline_with_offset : & str ,
2023-02-17 23:51:00 +08:00
include_placeholer : bool ,
2023-03-10 05:17:12 +08:00
) -> ( Tendril , Vec < SmallVec < [ ( usize , usize ) ; 1 ] > > ) {
let mut insert = Tendril ::new ( ) ;
2023-02-17 23:51:00 +08:00
let mut tabstops = Vec ::new ( ) ;
let mut offset = 0 ;
render_elements (
& snippet . elements ,
& mut insert ,
& mut offset ,
& mut tabstops ,
2023-03-10 05:17:12 +08:00
newline_with_offset ,
2023-02-17 23:51:00 +08:00
include_placeholer ,
) ;
2022-10-22 22:52:25 +08:00
2023-02-08 16:36:15 +08:00
// sort in ascending order (except for 0, which should always be the last one (per lsp doc))
2023-02-17 23:51:00 +08:00
tabstops . sort_unstable_by_key ( | ( n , _ ) | if * n = = 0 { usize ::MAX } else { * n } ) ;
2023-02-08 16:36:15 +08:00
// merge tabstops with the same index (we take advantage of the fact that we just sorted them
// above to simply look backwards)
2023-02-12 01:45:30 +08:00
let mut ntabstops = Vec ::< SmallVec < [ ( usize , usize ) ; 1 ] > > ::new ( ) ;
{
let mut prev = None ;
2023-02-17 23:51:00 +08:00
for ( tabstop , r ) in tabstops {
2023-02-12 01:45:30 +08:00
if prev = = Some ( tabstop ) {
let len_1 = ntabstops . len ( ) - 1 ;
2023-02-17 23:51:00 +08:00
ntabstops [ len_1 ] . push ( r ) ;
2023-02-12 01:45:30 +08:00
} else {
prev = Some ( tabstop ) ;
2023-02-17 23:51:00 +08:00
ntabstops . push ( smallvec! [ r ] ) ;
2023-02-12 01:45:30 +08:00
}
2023-02-08 16:36:15 +08:00
}
}
2023-02-17 23:51:00 +08:00
( insert , ntabstops )
2022-10-22 22:52:25 +08:00
}
2022-10-04 09:41:31 +08:00
mod parser {
2023-03-14 02:27:54 +08:00
use helix_core ::Tendril ;
2022-10-04 09:41:31 +08:00
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 > {
2023-02-08 03:15:39 +08:00
// var = [_a-zA-Z][_a-zA-Z0-9]*
2023-03-20 03:40:33 +08:00
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 )
2023-02-08 03:15:39 +08:00
}
}
2023-03-14 02:27:54 +08:00
const TEXT_ESCAPE_CHARS : & [ char ] = & [ '\\' , '}' , '$' ] ;
2023-03-20 03:32:46 +08:00
const CHOICE_TEXT_ESCAPE_CHARS : & [ char ] = & [ '\\' , '|' , ',' ] ;
2023-03-14 02:27:54 +08:00
2023-03-20 03:32:46 +08:00
fn text < ' a > (
escape_chars : & 'static [ char ] ,
term_chars : & 'static [ char ] ,
) -> impl Parser < ' a , Output = Tendril > {
2023-03-14 02:27:54 +08:00
move | input : & ' a str | {
2023-03-20 03:32:46 +08:00
let mut chars = input . char_indices ( ) . peekable ( ) ;
2023-03-14 02:27:54 +08:00
let mut res = Tendril ::new ( ) ;
while let Some ( ( i , c ) ) = chars . next ( ) {
match c {
'\\' = > {
2023-03-20 03:32:46 +08:00
if let Some ( & ( _ , c ) ) = chars . peek ( ) {
2023-03-14 02:27:54 +08:00
if escape_chars . contains ( & c ) {
2023-03-20 03:32:46 +08:00
chars . next ( ) ;
2023-03-14 02:27:54 +08:00
res . push ( c ) ;
continue ;
}
}
2023-03-20 03:32:46 +08:00
res . push ( '\\' ) ;
2023-03-14 02:27:54 +08:00
}
2023-03-20 03:32:46 +08:00
c if term_chars . contains ( & c ) = > return Ok ( ( & input [ i .. ] , res ) ) ,
2023-03-14 02:27:54 +08:00
c = > res . push ( c ) ,
}
}
Ok ( ( " " , res ) )
}
2022-10-04 09:41:31 +08:00
}
fn digit < ' a > ( ) -> impl Parser < ' a , Output = usize > {
2023-02-08 03:15:39 +08:00
filter_map ( take_while ( | c | c . is_ascii_digit ( ) ) , | s | s . parse ( ) . ok ( ) )
2022-10-04 09:41:31 +08:00
}
fn case_change < ' a > ( ) -> impl Parser < ' a , Output = CaseChange > {
use CaseChange ::* ;
choice! (
map ( " upcase " , | _ | Upcase ) ,
map ( " downcase " , | _ | Downcase ) ,
map ( " capitalize " , | _ | Capitalize ) ,
)
}
2023-03-20 03:32:46 +08:00
fn format < ' a > ( ) -> impl Parser < ' a , Output = FormatItem > {
2022-10-04 09:41:31 +08:00
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 (
2023-03-20 03:32:46 +08:00
seq! ( " ${ " , digit ( ) , " :+ " , text ( TEXT_ESCAPE_CHARS , & [ '}' ] ) , " } " ) ,
2022-10-04 09:41:31 +08:00
| seq | { Conditional ( seq . 1 , Some ( seq . 3 ) , None ) }
) ,
// '${' int ':?' if ':' else '}'
map (
seq! (
" ${ " ,
digit ( ) ,
" :? " ,
2023-03-20 03:32:46 +08:00
text ( TEXT_ESCAPE_CHARS , & [ ':' ] ) ,
2022-10-04 09:41:31 +08:00
" : " ,
2023-03-20 03:32:46 +08:00
text ( TEXT_ESCAPE_CHARS , & [ '}' ] ) ,
2022-10-04 09:41:31 +08:00
" } "
) ,
| seq | { Conditional ( seq . 1 , Some ( seq . 3 ) , Some ( seq . 5 ) ) }
) ,
// '${' int ':-' else '}' | '${' int ':' else '}'
map (
seq! (
" ${ " ,
digit ( ) ,
" : " ,
optional ( " - " ) ,
2023-03-20 03:32:46 +08:00
text ( TEXT_ESCAPE_CHARS , & [ '}' ] ) ,
2022-10-04 09:41:31 +08:00
" } "
) ,
| seq | { Conditional ( seq . 1 , None , Some ( seq . 4 ) ) }
) ,
)
}
2023-03-20 03:32:46 +08:00
fn regex < ' a > ( ) -> impl Parser < ' a , Output = Regex > {
2022-10-04 09:41:31 +08:00
map (
seq! (
" / " ,
2023-03-14 02:27:54 +08:00
// TODO parse as ECMAScript and convert to rust regex
2023-03-20 05:05:53 +08:00
text ( & [ '/' ] , & [ '/' ] ) ,
2022-10-04 09:41:31 +08:00
" / " ,
2023-03-20 05:05:53 +08:00
zero_or_more ( choice! (
2023-03-14 02:27:54 +08:00
format ( ) ,
2023-03-20 03:32:46 +08:00
// text doesn't parse $, if format fails we just accept the $ as text
map ( " $ " , | _ | FormatItem ::Text ( " $ " . into ( ) ) ) ,
map ( text ( & [ '\\' , '/' ] , & [ '/' , '$' ] ) , FormatItem ::Text ) ,
2023-03-14 02:27:54 +08:00
) ) ,
2022-10-04 09:41:31 +08:00
" / " ,
2023-03-20 03:32:46 +08:00
// 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 ( & [ ] , & [ '}' ] ) ,
2022-10-04 09:41:31 +08:00
) ,
| ( _ , 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 > > {
2023-02-21 14:04:24 +08:00
map (
seq! (
" ${ " ,
digit ( ) ,
" : " ,
2023-03-14 02:30:58 +08:00
// according to the grammar there is just a single anything here.
2023-03-16 07:16:22 +08:00
// However in the prose it is explained that placeholders can be nested.
// The example there contains both a placeholder text and a nested placeholder
2023-03-14 02:30:58 +08:00
// 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
2023-03-20 03:32:46 +08:00
zero_or_more ( anything ( TEXT_ESCAPE_CHARS , true ) ) ,
2023-02-21 14:04:24 +08:00
" } "
) ,
| seq | SnippetElement ::Placeholder {
2022-10-04 09:41:31 +08:00
tabstop : seq . 1 ,
2023-02-21 14:04:24 +08:00
value : seq . 3 ,
} ,
)
2022-10-04 09:41:31 +08:00
}
fn choice < ' a > ( ) -> impl Parser < ' a , Output = SnippetElement < ' a > > {
map (
seq! (
" ${ " ,
digit ( ) ,
" | " ,
2023-03-20 03:32:46 +08:00
sep ( text ( CHOICE_TEXT_ESCAPE_CHARS , & [ '|' , ',' ] ) , " , " ) ,
2022-10-04 09:41:31 +08:00
" |} " ,
) ,
| 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 ,
} ) ,
2023-03-20 05:06:29 +08:00
// ${var}
map ( seq! ( " ${ " , var ( ) , " } " , ) , | values | SnippetElement ::Variable {
name : values . 1 ,
default : None ,
regex : None ,
} ) ,
2022-10-04 09:41:31 +08:00
// ${var:default}
map (
2023-03-20 03:32:46 +08:00
seq! (
" ${ " ,
var ( ) ,
" : " ,
zero_or_more ( anything ( TEXT_ESCAPE_CHARS , true ) ) ,
" } " ,
) ,
2022-10-04 09:41:31 +08:00
| 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 ) ,
}
} ) ,
)
}
2023-03-20 03:32:46 +08:00
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 { & [ '$' ] } ;
2023-03-14 02:27:54 +08:00
move | input : & ' a str | {
let parser = choice! (
tabstop ( ) ,
placeholder ( ) ,
choice ( ) ,
variable ( ) ,
2023-03-20 03:32:46 +08:00
map ( " $ " , | _ | SnippetElement ::Text ( " $ " . into ( ) ) ) ,
map ( text ( escape_chars , term_chars ) , SnippetElement ::Text ) ,
2023-03-14 02:27:54 +08:00
) ;
2023-02-21 14:04:24 +08:00
parser . parse ( input )
}
2022-10-04 09:41:31 +08:00
}
fn snippet < ' a > ( ) -> impl Parser < ' a , Output = Snippet < ' a > > {
2023-03-20 03:32:46 +08:00
map ( one_or_more ( anything ( TEXT_ESCAPE_CHARS , false ) ) , | parts | {
Snippet { elements : parts }
2023-02-21 14:04:24 +08:00
} )
2022-10-04 09:41:31 +08:00
}
pub fn parse ( s : & str ) -> Result < Snippet , & str > {
2023-03-14 02:22:39 +08:00
snippet ( ) . parse ( s ) . and_then ( | ( remainder , snippet ) | {
if remainder . is_empty ( ) {
Ok ( snippet )
} else {
Err ( remainder )
}
} )
2022-10-04 09:41:31 +08:00
}
2023-03-14 02:27:54 +08:00
2022-10-04 09:41:31 +08:00
#[ 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 ! [
2023-03-14 02:27:54 +08:00
Text ( " match( " . into ( ) ) ,
2022-10-04 09:41:31 +08:00
Placeholder {
tabstop : 1 ,
2023-03-14 02:27:54 +08:00
value : vec ! ( Text ( " Arg1 " . into ( ) ) ) ,
2022-10-04 09:41:31 +08:00
} ,
2023-03-14 02:27:54 +08:00
Text ( " ) " . into ( ) )
2022-10-04 09:41:31 +08:00
]
} ) ,
parse ( " match(${1:Arg1}) " )
)
}
2023-03-14 02:22:39 +08:00
#[ test ]
2023-03-20 03:32:46 +08:00
fn unterminated_placeholder ( ) {
assert_eq! (
Ok ( Snippet {
elements : vec ! [ Text ( " match( " . into ( ) ) , Text ( " $ " . into ( ) ) , Text ( " {1:) " . into ( ) ) ]
} ) ,
parse ( " match(${1:) " )
)
2023-03-14 02:22:39 +08:00
}
#[ test ]
fn parse_empty_placeholder ( ) {
assert_eq! (
Ok ( Snippet {
elements : vec ! [
Text ( " match( " . into ( ) ) ,
Placeholder {
tabstop : 1 ,
value : vec ! [ ] ,
} ,
Text ( " ) " . into ( ) )
]
} ) ,
parse ( " match(${1:}) " )
)
}
2022-10-04 09:41:31 +08:00
#[ test ]
fn parse_placeholders_in_statement ( ) {
assert_eq! (
Ok ( Snippet {
elements : vec ! [
2023-03-14 02:27:54 +08:00
Text ( " local " . into ( ) ) ,
2022-10-04 09:41:31 +08:00
Placeholder {
tabstop : 1 ,
2023-03-14 02:27:54 +08:00
value : vec ! ( Text ( " var " . into ( ) ) ) ,
2022-10-04 09:41:31 +08:00
} ,
2023-03-14 02:27:54 +08:00
Text ( " = " . into ( ) ) ,
2022-10-04 09:41:31 +08:00
Placeholder {
tabstop : 1 ,
2023-03-14 02:27:54 +08:00
value : vec ! ( Text ( " value " . into ( ) ) ) ,
2022-10-04 09:41:31 +08:00
} ,
]
} ) ,
parse ( " local ${1:var} = ${1:value} " )
)
}
2023-02-17 23:51:00 +08:00
#[ test ]
fn parse_tabstop_nested_in_placeholder ( ) {
assert_eq! (
Ok ( Snippet {
elements : vec ! [ Placeholder {
tabstop : 1 ,
2023-03-14 02:27:54 +08:00
value : vec ! ( Text ( " var, " . into ( ) ) , Tabstop { tabstop : 2 } , ) ,
2023-02-17 23:51:00 +08:00
} , ]
} ) ,
parse ( " ${1:var, $2} " )
)
}
2023-02-21 14:04:24 +08:00
#[ test ]
fn parse_placeholder_nested_in_placeholder ( ) {
assert_eq! (
Ok ( Snippet {
elements : vec ! [ Placeholder {
tabstop : 1 ,
value : vec ! (
2023-03-14 02:27:54 +08:00
Text ( " foo " . into ( ) ) ,
2023-02-21 14:04:24 +08:00
Placeholder {
tabstop : 2 ,
2023-03-14 02:27:54 +08:00
value : vec ! ( Text ( " bar " . into ( ) ) ) ,
2023-02-21 14:04:24 +08:00
} ,
) ,
} , ]
} ) ,
parse ( " ${1:foo ${2:bar}} " )
)
}
2022-10-04 09:41:31 +08:00
#[ test ]
fn parse_all ( ) {
assert_eq! (
Ok ( Snippet {
elements : vec ! [
2023-03-14 02:27:54 +08:00
Text ( " hello " . into ( ) ) ,
2022-10-04 09:41:31 +08:00
Tabstop { tabstop : 1 } ,
Tabstop { tabstop : 2 } ,
2023-03-14 02:27:54 +08:00
Text ( " " . into ( ) ) ,
2022-10-04 09:41:31 +08:00
Choice {
tabstop : 1 ,
2023-03-14 02:27:54 +08:00
choices : vec ! [ " one " . into ( ) , " two " . into ( ) , " three " . into ( ) ]
2022-10-04 09:41:31 +08:00
} ,
2023-03-14 02:27:54 +08:00
Text ( " " . into ( ) ) ,
2022-10-04 09:41:31 +08:00
Variable {
name : " name " ,
2023-03-20 03:32:46 +08:00
default : Some ( vec! [ Text ( " foo " . into ( ) ) ] ) ,
2022-10-04 09:41:31 +08:00
regex : None
} ,
2023-03-14 02:27:54 +08:00
Text ( " " . into ( ) ) ,
2022-10-04 09:41:31 +08:00
Variable {
name : " var " ,
default : None ,
regex : None
} ,
2023-03-14 02:27:54 +08:00
Text ( " " . into ( ) ) ,
2022-10-04 09:41:31 +08:00
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 {
2023-03-14 02:27:54 +08:00
value : " (.*).+$ " . into ( ) ,
2023-03-20 03:32:46 +08:00
replacement : vec ! [ FormatItem ::Capture ( 1 ) , FormatItem ::Text ( " $ " . into ( ) ) ] ,
2023-03-14 02:27:54 +08:00
options : Tendril ::new ( ) ,
2022-10-04 09:41:31 +08:00
} ) ,
} ]
} ) ,
2023-03-20 03:32:46 +08:00
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 } " )
) ;
}
2023-03-20 05:06:44 +08:00
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 )
}
2023-03-20 03:32:46 +08:00
#[ test ]
fn robust_parsing ( ) {
2023-03-20 05:06:44 +08:00
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 " ,
2023-03-20 03:32:46 +08:00
) ;
2023-03-20 05:06:44 +08:00
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 ,
} ] ,
2022-10-04 09:41:31 +08:00
) ;
}
2023-03-20 05:06:44 +08:00
// TODO port more tests from https://github.com/microsoft/vscode/blob/dce493cb6e36346ef2714e82c42ce14fc461b15c/src/vs/editor/contrib/snippet/test/browser/snippetParser.test.ts
2022-10-04 09:41:31 +08:00
}
}