Auto pairs selection (#1254)

* use auto pairs with selections

Previously, the auto pairs code was converting the user selection into
its cursor form, and setting the transaction's selection to that cursor.
This has the effect of destroying the user's selection if they type a
pair character that gets auto completed.

This fixes the code to work with the user's selection, inserting auto
pairs where appropriate, but either keeping or extending the user's
selection.

* use movement::Direction instead of bool

* assume 0-width cursor is forward
pull/1330/head
Skyler Hawthorne 3 years ago committed by GitHub
parent 1c082cb4ef
commit 5b4540fc2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,7 +1,7 @@
//! When typing the opening character of one of the possible pairs defined below, //! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character. //! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction}; use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction};
use log::debug; use log::debug;
use smallvec::SmallVec; use smallvec::SmallVec;
@ -30,7 +30,6 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20
// [TODO] // [TODO]
// * delete implementation where it erases the whole bracket (|) -> | // * 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 // * 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 {% %} // middle of triple quotes, and more exotic pairs like Jinja's {% %}
@ -38,20 +37,18 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> { pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
debug!("autopairs hook selection: {:#?}", selection); debug!("autopairs hook selection: {:#?}", selection);
let cursors = selection.clone().cursors(doc.slice(..));
for &(open, close) in PAIRS { for &(open, close) in PAIRS {
if open == ch { if open == ch {
if open == close { if open == close {
return Some(handle_same(doc, &cursors, open, CLOSE_BEFORE, OPEN_BEFORE)); return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
} else { } else {
return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE)); return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
} }
} }
if close == ch { if close == ch {
// && char_at pos == close // && char_at pos == close
return Some(handle_close(doc, &cursors, open, close)); return Some(handle_close(doc, selection, open, close));
} }
} }
@ -66,6 +63,36 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
doc.get_char(pos - 1) doc.get_char(pos - 1)
} }
/// calculate what the resulting range should be for an auto pair insertion
fn get_next_range(
start_range: &Range,
offset: usize,
typed_char: char,
len_inserted: usize,
) -> Range {
let end_head = start_range.head + offset + typed_char.len_utf8();
let end_anchor = match (start_range.len(), start_range.direction()) {
// if we have a zero width cursor, it shifts to the same number
(0, _) => end_head,
// if we are inserting for a regular one-width cursor, the anchor
// moves with the head
(1, Direction::Forward) => end_head - 1,
(1, Direction::Backward) => end_head + 1,
// if we are appending, the anchor stays where it is; only offset
// for multiple range insertions
(_, Direction::Forward) => start_range.anchor + offset,
// when we are inserting in front of a selection, we need to move
// the anchor over by however many characters were inserted overall
(_, Direction::Backward) => start_range.anchor + offset + len_inserted,
};
Range::new(end_anchor, end_head)
}
fn handle_open( fn handle_open(
doc: &Rope, doc: &Rope,
selection: &Selection, selection: &Selection,
@ -74,36 +101,32 @@ fn handle_open(
close_before: &str, close_before: &str,
) -> Transaction { ) -> Transaction {
let mut end_ranges = SmallVec::with_capacity(selection.len()); let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0; let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| { let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let start_head = start_range.head; let cursor = start_range.cursor(doc.slice(..));
let next_char = doc.get_char(cursor);
let next = doc.get_char(start_head); let len_inserted;
let end_head = start_head + offs + open.len_utf8();
let end_anchor = if start_range.is_empty() {
end_head
} else {
start_range.anchor + offs
};
end_ranges.push(Range::new(end_anchor, end_head));
match next { let change = match next_char {
Some(ch) if !close_before.contains(ch) => { Some(ch) if !close_before.contains(ch) => {
offs += open.len_utf8(); len_inserted = open.len_utf8();
(start_head, start_head, Some(Tendril::from_char(open))) (cursor, cursor, Some(Tendril::from_char(open)))
} }
// None | Some(ch) if close_before.contains(ch) => {} // None | Some(ch) if close_before.contains(ch) => {}
_ => { _ => {
// insert open & close // insert open & close
let pair = Tendril::from_iter([open, close]); let pair = Tendril::from_iter([open, close]);
offs += open.len_utf8() + close.len_utf8(); len_inserted = open.len_utf8() + close.len_utf8();
(start_head, start_head, Some(pair)) (cursor, cursor, Some(pair))
} }
} };
let next_range = get_next_range(start_range, offs, open, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
change
}); });
let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
@ -117,28 +140,28 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
let mut offs = 0; let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| { let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let start_head = start_range.head; let cursor = start_range.cursor(doc.slice(..));
let next = doc.get_char(start_head); let next_char = doc.get_char(cursor);
let end_head = start_head + offs + close.len_utf8(); let mut len_inserted = 0;
let end_anchor = if start_range.is_empty() { let change = if next_char == Some(close) {
end_head // return transaction that moves past close
(cursor, cursor, None) // no-op
} else { } else {
start_range.anchor + offs len_inserted += close.len_utf8();
(cursor, cursor, Some(Tendril::from_char(close)))
}; };
end_ranges.push(Range::new(end_anchor, end_head)); let next_range = get_next_range(start_range, offs, close, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
if next == Some(close) { change
// return transaction that moves past close
(start_head, start_head, None) // no-op
} else {
offs += close.len_utf8();
(start_head, start_head, Some(Tendril::from_char(close)))
}
}); });
transaction.with_selection(Selection::new(end_ranges, selection.primary_index())) let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
debug!("auto pair transaction: {:#?}", t);
t
} }
/// handle cases where open and close is the same, or in triples ("""docstring""") /// handle cases where open and close is the same, or in triples ("""docstring""")
@ -154,42 +177,41 @@ fn handle_same(
let mut offs = 0; let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |start_range| { let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
let start_head = start_range.head; let cursor = start_range.cursor(doc.slice(..));
let end_head = start_head + offs + token.len_utf8(); let mut len_inserted = 0;
// if selection, retain anchor, if cursor, move over let next_char = doc.get_char(cursor);
let end_anchor = if start_range.is_empty() { let prev_char = prev_char(doc, cursor);
end_head
} else {
start_range.anchor + offs
};
end_ranges.push(Range::new(end_anchor, end_head)); let change = if next_char == Some(token) {
let next = doc.get_char(start_head);
let prev = prev_char(doc, start_head);
if next == Some(token) {
// return transaction that moves past close // return transaction that moves past close
(start_head, start_head, None) // no-op (cursor, cursor, None) // no-op
} else { } else {
let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32); let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32);
pair.push_char(token); pair.push_char(token);
// for equal pairs, don't insert both open and close if either // for equal pairs, don't insert both open and close if either
// side has a non-pair char // side has a non-pair char
if (next.is_none() || close_before.contains(next.unwrap())) if (next_char.is_none() || close_before.contains(next_char.unwrap()))
&& (prev.is_none() || open_before.contains(prev.unwrap())) && (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
{ {
pair.push_char(token); pair.push_char(token);
} }
offs += pair.len(); len_inserted += pair.len();
(start_head, start_head, Some(pair)) (cursor, cursor, Some(pair))
} };
let next_range = get_next_range(start_range, offs, token, len_inserted);
end_ranges.push(next_range);
offs += len_inserted;
change
}); });
transaction.with_selection(Selection::new(end_ranges, selection.primary_index())) let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
debug!("auto pair transaction: {:#?}", t);
t
} }
#[cfg(test)] #[cfg(test)]
@ -252,7 +274,20 @@ mod test {
&Selection::single(1, 0), &Selection::single(1, 0),
PAIRS, PAIRS,
|open, close| format!("{}{}", open, close), |open, close| format!("{}{}", open, close),
&Selection::single(1, 1), &Selection::single(2, 1),
);
}
/// [] -> 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("\n\n"),
&Selection::single(0, 2),
PAIRS,
|open, close| format!("\n{}{}\n", open, close),
&Selection::single(0, 3),
); );
} }
@ -276,26 +311,50 @@ mod test {
) )
}, },
&Selection::new( &Selection::new(
smallvec!(Range::point(1), Range::point(4), Range::point(7),), smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
0, 0,
), ),
); );
} }
// [TODO] broken until it works with selections
/// fo[o] -> append ( -> fo[o(]) /// fo[o] -> append ( -> fo[o(])
#[ignore]
#[test] #[test]
fn test_append() { fn test_append() {
test_hooks_with_pairs( test_hooks_with_pairs(
&Rope::from("foo"), &Rope::from("foo\n"),
&Selection::single(2, 4), &Selection::single(2, 4),
PAIRS, differing_pairs(),
|open, close| format!("foo{}{}", open, close), |open, close| format!("foo{}{}\n", open, close),
&Selection::single(2, 5), &Selection::single(2, 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(),
|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 ) -> ()[] /// ([]) -> insert ) -> ()[]
#[test] #[test]
fn test_insert_close_inside_pair() { fn test_insert_close_inside_pair() {
@ -307,7 +366,23 @@ mod test {
&Selection::single(2, 1), &Selection::single(2, 1),
*close, *close,
&doc, &doc,
&Selection::point(2), &Selection::single(3, 2),
);
}
}
/// [(]) -> append ) -> [()]
#[test]
fn test_append_close_inside_pair() {
for (open, close) in PAIRS {
let doc = Rope::from(format!("{}{}\n", open, close));
test_hooks(
&doc,
&Selection::single(0, 2),
*close,
&doc,
&Selection::single(0, 3),
); );
} }
} }
@ -323,8 +398,33 @@ mod test {
); );
let expected_sel = Selection::new( let expected_sel = Selection::new(
// smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),), 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);
}
}
/// [(]) [()]
/// [(]) -> 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, 0,
); );
@ -343,7 +443,7 @@ mod test {
#[test] #[test]
fn test_insert_open_inside_pair() { fn test_insert_open_inside_pair() {
let sel = Selection::single(2, 1); let sel = Selection::single(2, 1);
let expected_sel = Selection::point(2); let expected_sel = Selection::single(3, 2);
for (open, close) in differing_pairs() { for (open, close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", open, close)); let doc = Rope::from(format!("{}{}", open, close));
@ -357,11 +457,49 @@ mod test {
} }
} }
/// [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, &expected_doc, &expected_sel);
}
}
/// ([]) -> insert " -> ("[]") /// ([]) -> insert " -> ("[]")
#[test] #[test]
fn test_insert_nested_open_inside_pair() { fn test_insert_nested_open_inside_pair() {
let sel = Selection::single(2, 1); let sel = Selection::single(2, 1);
let expected_sel = Selection::point(2); 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, &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() { for (outer_open, outer_close) in differing_pairs() {
let doc = Rope::from(format!("{}{}", outer_open, outer_close,)); let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
@ -385,21 +523,44 @@ mod test {
&Selection::single(1, 0), &Selection::single(1, 0),
PAIRS, PAIRS,
|open, _| format!("{}word", open), |open, _| format!("{}word", open),
&Selection::point(1), &Selection::single(2, 1),
) )
} }
// [TODO] broken until it works with selections
/// [wor]d -> insert ( -> ([wor]d /// [wor]d -> insert ( -> ([wor]d
#[test] #[test]
#[ignore]
fn test_insert_open_with_selection() { fn test_insert_open_with_selection() {
test_hooks_with_pairs( test_hooks_with_pairs(
&Rope::from("word"), &Rope::from("word"),
&Selection::single(0, 4), &Selection::single(3, 0),
PAIRS, PAIRS,
|open, _| format!("{}word", open), |open, _| format!("{}word", open),
&Selection::single(1, 5), &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 PAIRS {
let doc = Rope::from("word");
let expected_doc = Rope::from(format!("wor{}d", close));
test_hooks(&doc, &sel, *close, &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(),
|open, close| format!("foo{}{} word", open, close),
&Selection::single(9, 4),
) )
} }
@ -413,7 +574,7 @@ mod test {
fn test_insert_open_after_non_pair() { fn test_insert_open_after_non_pair() {
let doc = Rope::from("word"); let doc = Rope::from("word");
let sel = Selection::single(5, 4); let sel = Selection::single(5, 4);
let expected_sel = Selection::point(5); let expected_sel = Selection::single(6, 5);
test_hooks_with_pairs( test_hooks_with_pairs(
&doc, &doc,
@ -431,4 +592,18 @@ mod test {
&expected_sel, &expected_sel,
); );
} }
/// appending with only a cursor should stay a cursor
///
/// [] -> append to end "foo -> "foo[]"
#[test]
fn test_append_single_cursor() {
test_hooks_with_pairs(
&Rope::from("\n"),
&Selection::single(0, 1),
PAIRS,
|open, close| format!("{}{}\n", open, close),
&Selection::single(1, 2),
);
}
} }

@ -7,6 +7,7 @@ use crate::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary, prev_grapheme_boundary,
}, },
movement::Direction,
Assoc, ChangeSet, RopeSlice, Assoc, ChangeSet, RopeSlice,
}; };
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
@ -82,6 +83,13 @@ impl Range {
std::cmp::max(self.anchor, self.head) std::cmp::max(self.anchor, self.head)
} }
/// Total length of the range.
#[inline]
#[must_use]
pub fn len(&self) -> usize {
self.to() - self.from()
}
/// The (inclusive) range of lines that the range overlaps. /// The (inclusive) range of lines that the range overlaps.
#[inline] #[inline]
#[must_use] #[must_use]
@ -102,6 +110,18 @@ impl Range {
self.anchor == self.head self.anchor == self.head
} }
/// `Direction::Backward` when head < anchor.
/// `Direction::Backward` otherwise.
#[inline]
#[must_use]
pub fn direction(&self) -> Direction {
if self.head < self.anchor {
Direction::Backward
} else {
Direction::Forward
}
}
/// Check two ranges for overlap. /// Check two ranges for overlap.
#[must_use] #[must_use]
pub fn overlaps(&self, other: &Self) -> bool { pub fn overlaps(&self, other: &Self) -> bool {

Loading…
Cancel
Save