@ -2,6 +2,7 @@
//! this module provides the functionality to insert the paired closing character.
use crate ::{ Range , Rope , Selection , Tendril , Transaction } ;
use log ::debug ;
use smallvec ::SmallVec ;
// Heavily based on https://github.com/codemirror/closebrackets/
@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[
( '`' , '`' ) ,
] ;
const CLOSE_BEFORE : & str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}" ; // includes space and newlines
// [TODO] build this dynamically in language config. see #992
const OPEN_BEFORE : & str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}" ;
const CLOSE_BEFORE : & str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}" ; // includes space and newlines
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
@ -25,40 +28,44 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202
//
// to simplify, maybe return Option<Transaction> and just reimplement the default
// TODO: delete implementation where it erases the whole bracket (|) -> |
// [TODO]
// * delete implementation where it erases the whole bracket (|) -> |
// * do not reduce to cursors; use whole selections, and surround with pair
// * change to multi character pairs to handle cases like placing the cursor in the
// middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[ must_use ]
pub fn hook ( doc : & Rope , selection : & Selection , ch : char ) -> Option < Transaction > {
debug ! ( "autopairs hook selection: {:#?}" , selection ) ;
let cursors = selection . clone ( ) . cursors ( doc . slice ( .. ) ) ;
for & ( open , close ) in PAIRS {
if open = = ch {
if open = = close {
return handle_same ( doc , selection , open ) ;
return Some ( handle_same ( doc , & cursors , open , CLOSE_BEFORE , OPEN_BEFORE ) ) ;
} else {
return Some ( handle_open ( doc , selection , open , close , CLOSE_BEFORE ) ) ;
return Some ( handle_open ( doc , & cursors , open , close , CLOSE_BEFORE ) ) ;
}
}
if close = = ch {
// && char_at pos == close
return Some ( handle_close ( doc , selection , open , close ) ) ;
return Some ( handle_close ( doc , & cursors , open , close ) ) ;
}
}
None
}
// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
// for example "&'a mut", or "fn<'a>"
fn next_char ( doc : & Rope , pos : usize ) -> Option < char > {
if pos > = doc . len_chars ( ) {
fn prev_char ( doc : & Rope , pos : usize ) -> Option < char > {
if pos = = 0 {
return None ;
}
Some ( doc . char ( pos ) )
doc . get_char ( pos - 1 )
}
// TODO: selections should be extended if range, moved if point.
// TODO: if not cursor but selection, wrap on both sides of selection (surround)
fn handle_open (
doc : & Rope ,
selection : & Selection ,
@ -66,98 +73,362 @@ fn handle_open(
close : char ,
close_before : & str ,
) -> Transaction {
let mut ranges = SmallVec ::with_capacity ( selection . len ( ) ) ;
let mut end_ ranges = SmallVec ::with_capacity ( selection . len ( ) ) ;
let mut offs = 0 ;
let transaction = Transaction ::change_by_selection ( doc , selection , | range | {
let pos = range . head ;
let next = next_char ( doc , pos ) ;
let transaction = Transaction ::change_by_selection ( doc , selection , | start_range | {
let start_head = start_range . head ;
let head = pos + offs + open . len_utf8 ( ) ;
// if selection, retain anchor, if cursor, move over
ranges . push ( Range ::new (
if range. is_empty ( ) {
head
let next = doc . get_char ( start_head ) ;
let end_head = start_head + offs + open . len_utf8 ( ) ;
let end_anchor = if start_ range. is_empty ( ) {
end_ head
} else {
range. anchor + offs
} ,
head ,
) ) ;
start_ range. anchor + offs
} ;
end_ranges . push ( Range ::new ( end_anchor , end_head ) ) ;
match next {
Some ( ch ) if ! close_before . contains ( ch ) = > {
offs + = 1 ;
// TODO: else return (use default handler that inserts open)
( pos , pos , Some ( Tendril ::from_char ( open ) ) )
offs + = open . len_utf8 ( ) ;
( start_head , start_head , Some ( Tendril ::from_char ( open ) ) )
}
// None | Some(ch) if close_before.contains(ch) => {}
_ = > {
// insert open & close
let mut pair = Tendril ::with_capacity ( 2 ) ;
pair . push_char ( open ) ;
pair . push_char ( close ) ;
offs + = 2 ;
( pos , pos , Some ( pair ) )
let pair = Tendril ::from_iter ( [ open , close ] ) ;
offs + = open . len_utf8 ( ) + close . len_utf8 ( ) ;
( start_head , start_head , Some ( pair ) )
}
}
} ) ;
transaction . with_selection ( Selection ::new ( ranges , selection . primary_index ( ) ) )
let t = transaction . with_selection ( Selection ::new ( end_ranges , selection . primary_index ( ) ) ) ;
debug ! ( "auto pair transaction: {:#?}" , t ) ;
t
}
fn handle_close ( doc : & Rope , selection : & Selection , _open : char , close : char ) -> Transaction {
let mut ranges = SmallVec ::with_capacity ( selection . len ( ) ) ;
let mut end_ ranges = SmallVec ::with_capacity ( selection . len ( ) ) ;
let mut offs = 0 ;
let transaction = Transaction ::change_by_selection ( doc , selection , | range | {
let pos = range . head ;
let next = next_char ( doc , pos ) ;
let transaction = Transaction ::change_by_selection ( doc , selection , | start_range | {
let start_head = start_range . head ;
let next = doc . get_char ( start_head ) ;
let end_head = start_head + offs + close . len_utf8 ( ) ;
let head = pos + offs + close . len_utf8 ( ) ;
// if selection, retain anchor, if cursor, move over
ranges . push ( Range ::new (
if range . is_empty ( ) {
head
let end_anchor = if start_range . is_empty ( ) {
end_head
} else {
range. anchor + offs
} ,
head ,
) ) ;
start_range . anchor + offs
} ;
end_ranges . push ( Range ::new ( end_anchor , end_head ) ) ;
if next = = Some ( close ) {
// return transaction that moves past close
( po s, po s, None ) // no-op
( start_head , start_head , None ) // no-op
} else {
offs + = close . len_utf8 ( ) ;
( start_head , start_head , Some ( Tendril ::from_char ( close ) ) )
}
} ) ;
// TODO: else return (use default handler that inserts close)
( pos , pos , Some ( Tendril ::from_char ( close ) ) )
transaction . with_selection ( Selection ::new ( end_ranges , selection . primary_index ( ) ) )
}
/// handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same (
doc : & Rope ,
selection : & Selection ,
token : char ,
close_before : & str ,
open_before : & str ,
) -> Transaction {
let mut end_ranges = SmallVec ::with_capacity ( selection . len ( ) ) ;
let mut offs = 0 ;
let transaction = Transaction ::change_by_selection ( doc , selection , | start_range | {
let start_head = start_range . head ;
let end_head = start_head + offs + token . len_utf8 ( ) ;
// if selection, retain anchor, if cursor, move over
let end_anchor = if start_range . is_empty ( ) {
end_head
} else {
start_range . anchor + offs
} ;
end_ranges . push ( Range ::new ( end_anchor , end_head ) ) ;
let next = doc . get_char ( start_head ) ;
let prev = prev_char ( doc , start_head ) ;
if next = = Some ( token ) {
// return transaction that moves past close
( start_head , start_head , None ) // no-op
} else {
let mut pair = Tendril ::with_capacity ( 2 * token . len_utf8 ( ) as u32 ) ;
pair . push_char ( token ) ;
// for equal pairs, don't insert both open and close if either
// side has a non-pair char
if ( next . is_none ( ) | | close_before . contains ( next . unwrap ( ) ) )
& & ( prev . is_none ( ) | | open_before . contains ( prev . unwrap ( ) ) )
{
pair . push_char ( token ) ;
}
offs + = pair . len ( ) ;
( start_head , start_head , Some ( pair ) )
}
} ) ;
transaction . with_selection ( Selection ::new ( ranges , selection . primary_index ( ) ) )
}
// handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same ( _doc : & Rope , _selection : & Selection , _token : char ) -> Option < Transaction > {
// if not cursor but selection, wrap
// let next = next char
// if next == bracket {
// // if start of syntax node, insert token twice (new pair because node is complete)
// // elseif colsedBracketAt
// // is_triple == allow triple && next 3 is equal
// // cursor jump over
// }
//} else if allow_triple && followed by triple {
//}
//} else if next != word char && prev != bracket && prev != word char {
// // condition checks for cases like I' where you don't want I'' (or I'm)
// insert pair ("")
//}
None
transaction . with_selection ( Selection ::new ( end_ranges , selection . primary_index ( ) ) )
}
#[ cfg(test) ]
mod test {
use super ::* ;
use smallvec ::smallvec ;
fn differing_pairs ( ) -> impl Iterator < Item = & ' static ( char , char ) > {
PAIRS . iter ( ) . filter ( | ( open , close ) | open ! = close )
}
fn matching_pairs ( ) -> impl Iterator < Item = & ' static ( char , char ) > {
PAIRS . iter ( ) . filter ( | ( open , close ) | open = = close )
}
fn test_hooks (
in_doc : & Rope ,
in_sel : & Selection ,
ch : char ,
expected_doc : & Rope ,
expected_sel : & Selection ,
) {
let trans = hook ( & in_doc , & in_sel , ch ) . unwrap ( ) ;
let mut actual_doc = in_doc . clone ( ) ;
assert! ( trans . apply ( & mut actual_doc ) ) ;
assert_eq! ( expected_doc , & actual_doc ) ;
assert_eq! ( expected_sel , trans . selection ( ) . unwrap ( ) ) ;
}
fn test_hooks_with_pairs < I , F , R > (
in_doc : & Rope ,
in_sel : & Selection ,
pairs : I ,
get_expected_doc : F ,
actual_sel : & Selection ,
) where
I : IntoIterator < Item = & ' static ( char , char ) > ,
F : Fn ( char , char ) -> R ,
R : Into < Rope > ,
Rope : From < R > ,
{
pairs . into_iter ( ) . for_each ( | ( open , close ) | {
test_hooks (
in_doc ,
in_sel ,
* open ,
& Rope ::from ( get_expected_doc ( * open , * close ) ) ,
actual_sel ,
)
} ) ;
}
// [] indicates range
/// [] -> insert ( -> ([])
#[ test ]
fn test_insert_blank ( ) {
test_hooks_with_pairs (
& Rope ::new ( ) ,
& Selection ::single ( 1 , 0 ) ,
PAIRS ,
| open , close | format! ( "{}{}" , open , close ) ,
& Selection ::single ( 1 , 1 ) ,
) ;
}
/// [] ([])
/// [] -> insert -> ([])
/// [] ([])
#[ test ]
fn test_insert_blank_multi_cursor ( ) {
test_hooks_with_pairs (
& Rope ::from ( "\n\n\n" ) ,
& Selection ::new (
smallvec ! ( Range ::new ( 1 , 0 ) , Range ::new ( 2 , 1 ) , Range ::new ( 3 , 2 ) , ) ,
0 ,
) ,
PAIRS ,
| open , close | {
format! (
"{open}{close}\n{open}{close}\n{open}{close}\n" ,
open = open ,
close = close
)
} ,
& Selection ::new (
smallvec ! ( Range ::point ( 1 ) , Range ::point ( 4 ) , Range ::point ( 7 ) , ) ,
0 ,
) ,
) ;
}
// [TODO] broken until it works with selections
/// fo[o] -> append ( -> fo[o(])
#[ ignore ]
#[ test ]
fn test_append ( ) {
test_hooks_with_pairs (
& Rope ::from ( "foo" ) ,
& Selection ::single ( 2 , 4 ) ,
PAIRS ,
| open , close | format! ( "foo{}{}" , open , close ) ,
& Selection ::single ( 2 , 5 ) ,
) ;
}
/// ([]) -> insert ) -> ()[]
#[ test ]
fn test_insert_close_inside_pair ( ) {
for ( open , close ) in PAIRS {
let doc = Rope ::from ( format! ( "{}{}" , open , close ) ) ;
test_hooks (
& doc ,
& Selection ::single ( 2 , 1 ) ,
* close ,
& doc ,
& Selection ::point ( 2 ) ,
) ;
}
}
/// ([]) ()[]
/// ([]) -> insert ) -> ()[]
/// ([]) ()[]
#[ test ]
fn test_insert_close_inside_pair_multi_cursor ( ) {
let sel = Selection ::new (
smallvec ! ( Range ::new ( 2 , 1 ) , Range ::new ( 5 , 4 ) , Range ::new ( 8 , 7 ) , ) ,
0 ,
) ;
let expected_sel = Selection ::new (
// smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
smallvec ! ( Range ::point ( 2 ) , Range ::point ( 5 ) , Range ::point ( 8 ) , ) ,
0 ,
) ;
for ( open , close ) in PAIRS {
let doc = Rope ::from ( format! (
"{open}{close}\n{open}{close}\n{open}{close}\n" ,
open = open ,
close = close
) ) ;
test_hooks ( & doc , & sel , * close , & doc , & expected_sel ) ;
}
}
/// ([]) -> insert ( -> (([]))
#[ test ]
fn test_insert_open_inside_pair ( ) {
let sel = Selection ::single ( 2 , 1 ) ;
let expected_sel = Selection ::point ( 2 ) ;
for ( open , close ) in differing_pairs ( ) {
let doc = Rope ::from ( format! ( "{}{}" , open , close ) ) ;
let expected_doc = Rope ::from ( format! (
"{open}{open}{close}{close}" ,
open = open ,
close = close
) ) ;
test_hooks ( & doc , & sel , * open , & expected_doc , & expected_sel ) ;
}
}
/// ([]) -> insert " -> ("[]")
#[ test ]
fn test_insert_nested_open_inside_pair ( ) {
let sel = Selection ::single ( 2 , 1 ) ;
let expected_sel = Selection ::point ( 2 ) ;
for ( outer_open , outer_close ) in differing_pairs ( ) {
let doc = Rope ::from ( format! ( "{}{}" , outer_open , outer_close , ) ) ;
for ( inner_open , inner_close ) in matching_pairs ( ) {
let expected_doc = Rope ::from ( format! (
"{}{}{}{}" ,
outer_open , inner_open , inner_close , outer_close
) ) ;
test_hooks ( & doc , & sel , * inner_open , & expected_doc , & expected_sel ) ;
}
}
}
/// []word -> insert ( -> ([]word
#[ test ]
fn test_insert_open_before_non_pair ( ) {
test_hooks_with_pairs (
& Rope ::from ( "word" ) ,
& Selection ::single ( 1 , 0 ) ,
PAIRS ,
| open , _ | format! ( "{}word" , open ) ,
& Selection ::point ( 1 ) ,
)
}
// [TODO] broken until it works with selections
/// [wor]d -> insert ( -> ([wor]d
#[ test ]
#[ ignore ]
fn test_insert_open_with_selection ( ) {
test_hooks_with_pairs (
& Rope ::from ( "word" ) ,
& Selection ::single ( 0 , 4 ) ,
PAIRS ,
| open , _ | format! ( "{}word" , open ) ,
& Selection ::single ( 1 , 5 ) ,
)
}
/// we want pairs that are *not* the same char to be inserted after
/// a non-pair char, for cases like functions, but for pairs that are
/// the same char, we want to *not* insert a pair to handle cases like "I'm"
///
/// word[] -> insert ( -> word([])
/// word[] -> insert ' -> word'[]
#[ test ]
fn test_insert_open_after_non_pair ( ) {
let doc = Rope ::from ( "word" ) ;
let sel = Selection ::single ( 5 , 4 ) ;
let expected_sel = Selection ::point ( 5 ) ;
test_hooks_with_pairs (
& doc ,
& sel ,
differing_pairs ( ) ,
| open , close | format! ( "word{}{}" , open , close ) ,
& expected_sel ,
) ;
test_hooks_with_pairs (
& doc ,
& sel ,
matching_pairs ( ) ,
| open , _ | format! ( "word{}" , open ) ,
& expected_sel ,
) ;
}
}