Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/auto_pairs.rs')
-rw-r--r--helix-core/src/auto_pairs.rs630
1 files changed, 597 insertions, 33 deletions
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index 85329040..bcd47356 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -1,12 +1,16 @@
//! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character.
-use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction};
+use crate::{
+ graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
+};
use std::collections::HashMap;
+use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
+
pub const DEFAULT_PAIRS: &[(char, char)] = &[
('(', ')'),
('{', '}'),
@@ -17,7 +21,7 @@ pub const DEFAULT_PAIRS: &[(char, char)] = &[
];
/// The type that represents the collection of auto pairs,
-/// keyed by both opener and closer.
+/// keyed by the opener.
#[derive(Debug, Clone)]
pub struct AutoPairs(HashMap<char, Pair>);
@@ -75,9 +79,9 @@ impl From<(&char, &char)> for Pair {
impl AutoPairs {
/// Make a new AutoPairs set with the given pairs and default conditions.
- pub fn new<'a, V, A>(pairs: V) -> Self
+ pub fn new<'a, V: 'a, A>(pairs: V) -> Self
where
- V: IntoIterator<Item = A> + 'a,
+ V: IntoIterator<Item = A>,
A: Into<Pair>,
{
let mut auto_pairs = HashMap::new();
@@ -121,7 +125,7 @@ impl Default for AutoPairs {
#[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option<Transaction> {
- log::trace!("autopairs hook selection: {:#?}", selection);
+ debug!("autopairs hook selection: {:#?}", selection);
if let Some(pair) = pairs.get(ch) {
if pair.same() {
@@ -145,8 +149,22 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
doc.get_char(pos - 1)
}
+fn is_single_grapheme(doc: &Rope, range: &Range) -> bool {
+ let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to()));
+ let first = graphemes.next();
+ let second = graphemes.next();
+ debug!("first: {:#?}, second: {:#?}", first, second);
+ first.is_some() && second.is_none()
+}
+
/// calculate what the resulting range should be for an auto pair insertion
-fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted: usize) -> Range {
+fn get_next_range(
+ doc: &Rope,
+ start_range: &Range,
+ offset: usize,
+ typed_char: char,
+ len_inserted: usize,
+) -> Range {
// When the character under the cursor changes due to complete pair
// insertion, we must look backward a grapheme and then add the length
// of the insertion to put the resulting cursor in the right place, e.g.
@@ -166,13 +184,13 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// inserting at the very end of the document after the last newline
if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
return Range::new(
- start_range.anchor + offset + 1,
- start_range.head + offset + 1,
+ start_range.anchor + offset + typed_char.len_utf8(),
+ start_range.head + offset + typed_char.len_utf8(),
);
}
+ let single_grapheme = is_single_grapheme(doc, start_range);
let doc_slice = doc.slice(..);
- let single_grapheme = start_range.is_single_grapheme(doc_slice);
// just skip over graphemes
if len_inserted == 0 {
@@ -197,28 +215,29 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// trivial case: only inserted a single-char opener, just move the selection
if len_inserted == 1 {
let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
- start_range.anchor + offset + 1
+ start_range.anchor + offset + typed_char.len_utf8()
} else {
start_range.anchor + offset
};
- return Range::new(end_anchor, start_range.head + offset + 1);
+ return Range::new(
+ end_anchor,
+ start_range.head + offset + typed_char.len_utf8(),
+ );
}
// If the head = 0, then we must be in insert mode with a backward
// cursor, which implies the head will just move
let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
- start_range.head + offset + 1
+ start_range.head + offset + typed_char.len_utf8()
} else {
// We must have a forward cursor, which means we must move to the
// other end of the grapheme to get to where the new characters
// are inserted, then move the head to where it should be
let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
- log::trace!(
+ debug!(
"prev_bound: {}, offset: {}, len_inserted: {}",
- prev_bound,
- offset,
- len_inserted
+ prev_bound, offset, len_inserted
);
prev_bound + offset + len_inserted
};
@@ -234,7 +253,8 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
(_, Direction::Forward) => {
if single_grapheme {
- graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head) + 1
+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head)
+ + typed_char.len_utf8()
// if we are appending, the anchor stays where it is; only offset
// for multiple range insertions
@@ -248,9 +268,7 @@ fn get_next_range(doc: &Rope, start_range: &Range, offset: usize, len_inserted:
// if we're backward, then the head is at the first char
// of the typed char, so we need to add the length of
// the closing char
- graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor)
- + len_inserted
- + offset
+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
} else {
// when we are inserting in front of a selection, we need to move
// the anchor over by however many characters were inserted overall
@@ -271,12 +289,9 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let next_char = doc.get_char(cursor);
let len_inserted;
- // Since auto pairs are currently limited to single chars, we're either
- // inserting exactly one or two chars. When arbitrary length pairs are
- // added, these will need to be changed.
let change = match next_char {
Some(_) if !pair.should_close(doc, start_range) => {
- len_inserted = 1;
+ len_inserted = pair.open.len_utf8();
let mut tendril = Tendril::new();
tendril.push(pair.open);
(cursor, cursor, Some(tendril))
@@ -284,12 +299,12 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
_ => {
// insert open & close
let pair_str = Tendril::from_iter([pair.open, pair.close]);
- len_inserted = 2;
+ len_inserted = pair.open.len_utf8() + pair.close.len_utf8();
(cursor, cursor, Some(pair_str))
}
};
- let next_range = get_next_range(doc, start_range, offs, len_inserted);
+ let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -297,12 +312,13 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
+ debug!("auto pair transaction: {:#?}", t);
t
}
fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len());
+
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
@@ -314,13 +330,13 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
// return transaction that moves past close
(cursor, cursor, None) // no-op
} else {
- len_inserted = 1;
+ len_inserted += pair.close.len_utf8();
let mut tendril = Tendril::new();
tendril.push(pair.close);
(cursor, cursor, Some(tendril))
};
- let next_range = get_next_range(doc, start_range, offs, len_inserted);
+ let next_range = get_next_range(doc, start_range, offs, pair.close, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -328,7 +344,7 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
+ debug!("auto pair transaction: {:#?}", t);
t
}
@@ -356,11 +372,11 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
pair_str.push(pair.close);
}
- len_inserted += pair_str.chars().count();
+ len_inserted += pair_str.len();
(cursor, cursor, Some(pair_str))
};
- let next_range = get_next_range(doc, start_range, offs, len_inserted);
+ let next_range = get_next_range(doc, start_range, offs, pair.open, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
@@ -368,6 +384,554 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction {
});
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
- log::debug!("auto pair transaction: {:#?}", t);
+ debug!("auto pair transaction: {:#?}", t);
t
}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use smallvec::smallvec;
+
+ const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
+
+ fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ DEFAULT_PAIRS.iter().filter(|(open, close)| open != close)
+ }
+
+ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ DEFAULT_PAIRS.iter().filter(|(open, close)| open == close)
+ }
+
+ fn test_hooks(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ ch: char,
+ pairs: &[(char, char)],
+ expected_doc: &Rope,
+ expected_sel: &Selection,
+ ) {
+ let pairs = AutoPairs::new(pairs.iter());
+ let trans = hook(in_doc, in_sel, ch, &pairs).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,
+ test_pairs: I,
+ pairs: &[(char, char)],
+ 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>,
+ {
+ test_pairs.into_iter().for_each(|(open, close)| {
+ test_hooks(
+ in_doc,
+ in_sel,
+ *open,
+ pairs,
+ &Rope::from(get_expected_doc(*open, *close)),
+ actual_sel,
+ )
+ });
+ }
+
+ // [] indicates range
+
+ /// [] -> insert ( -> ([])
+ #[test]
+ fn test_insert_blank() {
+ test_hooks_with_pairs(
+ &Rope::from(LINE_END),
+ &Selection::single(1, 0),
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
+ |open, close| format!("{}{}{}", open, close, LINE_END),
+ &Selection::single(2, 1),
+ );
+
+ let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END));
+
+ test_hooks_with_pairs(
+ &empty_doc,
+ &Selection::single(empty_doc.len_chars(), LINE_END.len()),
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
+ |open, close| {
+ format!(
+ "{line_end}{open}{close}{line_end}",
+ open = open,
+ close = close,
+ line_end = LINE_END
+ )
+ },
+ &Selection::single(LINE_END.len() + 2, LINE_END.len() + 1),
+ );
+ }
+
+ #[test]
+ fn test_insert_before_multi_code_point_graphemes() {
+ for (_, close) in differing_pairs() {
+ test_hooks(
+ &Rope::from(format!("hello ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ goodbye{}", LINE_END)),
+ &Selection::single(13, 6),
+ *close,
+ DEFAULT_PAIRS,
+ &Rope::from(format!("hello {}๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ goodbye{}", close, LINE_END)),
+ &Selection::single(14, 7),
+ );
+ }
+ }
+
+ #[test]
+ fn test_insert_at_end_of_document() {
+ test_hooks_with_pairs(
+ &Rope::from(LINE_END),
+ &Selection::single(LINE_END.len(), LINE_END.len()),
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
+ |open, close| format!("{}{}{}", LINE_END, open, close),
+ &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
+ );
+
+ test_hooks_with_pairs(
+ &Rope::from(format!("foo{}", LINE_END)),
+ &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
+ |open, close| format!("foo{}{}{}", LINE_END, open, close),
+ &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
+ );
+ }
+
+ /// [] -> append ( -> ([])
+ #[test]
+ fn test_append_blank() {
+ test_hooks_with_pairs(
+ // this is what happens when you have a totally blank document and then append
+ &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
+ // before inserting the pair, the cursor covers all of both empty lines
+ &Selection::single(0, LINE_END.len() * 2),
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
+ |open, close| {
+ format!(
+ "{line_end}{open}{close}{line_end}",
+ line_end = LINE_END,
+ open = open,
+ close = close
+ )
+ },
+ // after inserting pair, the cursor covers the first new line and the open char
+ &Selection::single(0, LINE_END.len() + 2),
+ );
+ }
+
+ /// [] ([])
+ /// [] -> 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,
+ ),
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
+ |open, close| {
+ format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+ 0,
+ ),
+ );
+ }
+
+ /// fo[o] -> append ( -> fo[o(])
+ #[test]
+ fn test_append() {
+ test_hooks_with_pairs(
+ &Rope::from("foo\n"),
+ &Selection::single(2, 4),
+ differing_pairs(),
+ DEFAULT_PAIRS,
+ |open, close| format!("foo{}{}\n", open, close),
+ &Selection::single(2, 5),
+ );
+ }
+
+ /// foo[] -> append to end of line ( -> foo([])
+ #[test]
+ fn test_append_single_cursor() {
+ test_hooks_with_pairs(
+ &Rope::from(format!("foo{}", LINE_END)),
+ &Selection::single(3, 3 + LINE_END.len()),
+ differing_pairs(),
+ DEFAULT_PAIRS,
+ |open, close| format!("foo{}{}{}", open, close, LINE_END),
+ &Selection::single(4, 5),
+ );
+ }
+
+ /// fo[o] fo[o(])
+ /// fo[o] -> append ( -> fo[o(])
+ /// fo[o] fo[o(])
+ #[test]
+ fn test_append_multi() {
+ test_hooks_with_pairs(
+ &Rope::from("foo\nfoo\nfoo\n"),
+ &Selection::new(
+ smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)),
+ 0,
+ ),
+ differing_pairs(),
+ DEFAULT_PAIRS,
+ |open, close| {
+ format!(
+ "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)),
+ 0,
+ ),
+ );
+ }
+
+ /// ([)] -> insert ) -> ()[]
+ #[test]
+ fn test_insert_close_inside_pair() {
+ for (open, close) in DEFAULT_PAIRS {
+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
+
+ test_hooks(
+ &doc,
+ &Selection::single(2, 1),
+ *close,
+ DEFAULT_PAIRS,
+ &doc,
+ &Selection::single(2 + LINE_END.len(), 2),
+ );
+ }
+ }
+
+ /// [(]) -> append ) -> [()]
+ #[test]
+ fn test_append_close_inside_pair() {
+ for (open, close) in DEFAULT_PAIRS {
+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
+
+ test_hooks(
+ &doc,
+ &Selection::single(0, 2),
+ *close,
+ DEFAULT_PAIRS,
+ &doc,
+ &Selection::single(0, 2 + LINE_END.len()),
+ );
+ }
+ }
+
+ /// ([]) ()[]
+ /// ([]) -> 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),),
+ 0,
+ );
+
+ for (open, close) in DEFAULT_PAIRS {
+ let doc = Rope::from(format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel);
+ }
+ }
+
+ /// [(]) [()]
+ /// [(]) -> append ) -> [()]
+ /// [(]) [()]
+ #[test]
+ fn test_append_close_inside_pair_multi_cursor() {
+ let sel = Selection::new(
+ smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),),
+ 0,
+ );
+
+ let expected_sel = Selection::new(
+ smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),),
+ 0,
+ );
+
+ for (open, close) in DEFAULT_PAIRS {
+ let doc = Rope::from(format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *close, DEFAULT_PAIRS, &doc, &expected_sel);
+ }
+ }
+
+ /// ([]) -> insert ( -> (([]))
+ #[test]
+ fn test_insert_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::single(3, 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,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
+ }
+ }
+
+ /// [word(]) -> append ( -> [word((]))
+ #[test]
+ fn test_append_open_inside_pair() {
+ let sel = Selection::single(0, 6);
+ let expected_sel = Selection::single(0, 7);
+
+ for (open, close) in differing_pairs() {
+ let doc = Rope::from(format!("word{}{}", open, close));
+ let expected_doc = Rope::from(format!(
+ "word{open}{open}{close}{close}",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(
+ &doc,
+ &sel,
+ *open,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
+ }
+ }
+
+ /// ([]) -> insert " -> ("[]")
+ #[test]
+ fn test_insert_nested_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::single(3, 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,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
+ }
+ }
+ }
+
+ /// [(]) -> append " -> [("]")
+ #[test]
+ fn test_append_nested_open_inside_pair() {
+ let sel = Selection::single(0, 2);
+ let expected_sel = Selection::single(0, 3);
+
+ 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,
+ DEFAULT_PAIRS,
+ &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),
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(2, 1),
+ )
+ }
+
+ /// [wor]d -> insert ( -> ([wor]d
+ #[test]
+ fn test_insert_open_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(3, 0),
+ DEFAULT_PAIRS,
+ DEFAULT_PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(4, 1),
+ )
+ }
+
+ /// [wor]d -> append ) -> [wor)]d
+ #[test]
+ fn test_append_close_inside_non_pair_with_selection() {
+ let sel = Selection::single(0, 4);
+ let expected_sel = Selection::single(0, 5);
+
+ for (_, close) in DEFAULT_PAIRS {
+ let doc = Rope::from("word");
+ let expected_doc = Rope::from(format!("wor{}d", close));
+ test_hooks(
+ &doc,
+ &sel,
+ *close,
+ DEFAULT_PAIRS,
+ &expected_doc,
+ &expected_sel,
+ );
+ }
+ }
+
+ /// foo[ wor]d -> insert ( -> foo([) wor]d
+ #[test]
+ fn test_insert_open_trailing_word_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("foo word"),
+ &Selection::single(7, 3),
+ differing_pairs(),
+ DEFAULT_PAIRS,
+ |open, close| format!("foo{}{} word", open, close),
+ &Selection::single(9, 4),
+ )
+ }
+
+ /// foo([) wor]d -> insert ) -> foo()[ wor]d
+ #[test]
+ fn test_insert_close_inside_pair_trailing_word_with_selection() {
+ for (open, close) in differing_pairs() {
+ test_hooks(
+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
+ &Selection::single(9, 4),
+ *close,
+ DEFAULT_PAIRS,
+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
+ &Selection::single(9, 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(format!("word{}", LINE_END));
+ let sel = Selection::single(5, 4);
+ let expected_sel = Selection::single(6, 5);
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ differing_pairs(),
+ DEFAULT_PAIRS,
+ |open, close| format!("word{}{}{}", open, close, LINE_END),
+ &expected_sel,
+ );
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ matching_pairs(),
+ DEFAULT_PAIRS,
+ |open, _| format!("word{}{}", open, LINE_END),
+ &expected_sel,
+ );
+ }
+
+ #[test]
+ fn test_configured_pairs() {
+ let test_pairs = &[('`', ':'), ('+', '-')];
+
+ test_hooks_with_pairs(
+ &Rope::from(LINE_END),
+ &Selection::single(1, 0),
+ test_pairs,
+ test_pairs,
+ |open, close| format!("{}{}{}", open, close, LINE_END),
+ &Selection::single(2, 1),
+ );
+
+ let doc = Rope::from(format!("foo`: word{}", LINE_END));
+
+ test_hooks(
+ &doc,
+ &Selection::single(9, 4),
+ ':',
+ test_pairs,
+ &doc,
+ &Selection::single(9, 5),
+ )
+ }
+}