@@ -1,7 +1,9 @@
1//! When typing the opening character of one of the possible pairs defined below,
1//! When typing the opening character of one of the possible pairs defined below,
2//! this module provides the functionality to insert the paired closing character.
2//! this module provides the functionality to insert the paired closing character.
4−use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction};
5+ graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
5use log::debug;
7use log::debug;
6use smallvec::SmallVec;
8use smallvec::SmallVec;
@@ -63,31 +65,132 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
63 doc.get_char(pos - 1)
65 doc.get_char(pos - 1)
68+fn is_single_grapheme(doc: &Rope, range: &Range) -> bool {
69+ let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to()));
70+ let first = graphemes.next();
71+ let second = graphemes.next();
72+ debug!("first: {:#?}, second: {:#?}", first, second);
73+ first.is_some() && second.is_none()
66/// calculate what the resulting range should be for an auto pair insertion
76/// calculate what the resulting range should be for an auto pair insertion
67fn get_next_range(
77fn get_next_range(
68 start_range: &Range,
79 start_range: &Range,
69 offset: usize,
80 offset: usize,
70 typed_char: char,
81 typed_char: char,
71 len_inserted: usize,
82 len_inserted: usize,
72) -> Range {
83) -> Range {
84+ // When the character under the cursor changes due to complete pair
85+ // insertion, we must look backward a grapheme and then add the length
86+ // of the insertion to put the resulting cursor in the right place, e.g.
88+ // foo[\r\n] - anchor: 3, head: 5
89+ // foo([)]\r\n - anchor: 4, head: 5
91+ // foo[\r\n] - anchor: 3, head: 5
92+ // foo'[\r\n] - anchor: 4, head: 6
94+ // foo([)]\r\n - anchor: 4, head: 5
95+ // foo()[\r\n] - anchor: 5, head: 7
97+ // [foo]\r\n - anchor: 0, head: 3
98+ // [foo(])\r\n - anchor: 0, head: 5
100+ // inserting at the very end of the document after the last newline
101+ if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
103+ start_range.anchor + offset + typed_char.len_utf8(),
104+ start_range.head + offset + typed_char.len_utf8(),
108+ let single_grapheme = is_single_grapheme(doc, start_range);
109+ let doc_slice = doc.slice(..);
111+ // just skip over graphemes
112+ if len_inserted == 0 {
113+ let end_anchor = if single_grapheme {
114+ graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
116+ // even for backward inserts with multiple grapheme selections,
117+ // we want the anchor to stay where it is so that the relative
118+ // selection does not change, e.g.:
120+ // foo([) wor]d -> insert ) -> foo()[ wor]d
122+ start_range.anchor + offset
127+ graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
131+ // trivial case: only inserted a single-char opener, just move the selection
132+ if len_inserted == 1 {
133+ let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
134+ start_range.anchor + offset + typed_char.len_utf8()
136+ start_range.anchor + offset
141+ start_range.head + offset + typed_char.len_utf8(),
145+ // If the head = 0, then we must be in insert mode with a backward
146+ // cursor, which implies the head will just move
147+ let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
73− let end_head = start_range.head + offset + typed_char.len_utf8();
148+ start_range.head + offset + typed_char.len_utf8()
150+ // We must have a forward cursor, which means we must move to the
151+ // other end of the grapheme to get to where the new characters
152+ // are inserted, then move the head to where it should be
153+ let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
155+ "prev_bound: {}, offset: {}, len_inserted: {}",
156+ prev_bound, offset, len_inserted
158+ prev_bound + offset + len_inserted
75 let end_anchor = match (start_range.len(), start_range.direction()) {
161 let end_anchor = match (start_range.len(), start_range.direction()) {
76 // if we have a zero width cursor, it shifts to the same number
162 // if we have a zero width cursor, it shifts to the same number
77 (0, _) => end_head,
163 (0, _) => end_head,
79− // if we are inserting for a regular one-width cursor, the anchor
165+ // If we are inserting for a regular one-width cursor, the anchor
80− // moves with the head
166+ // moves with the head. This is the fast path for ASCII.
81 (1, Direction::Forward) => end_head - 1,
167 (1, Direction::Forward) => end_head - 1,
82 (1, Direction::Backward) => end_head + 1,
168 (1, Direction::Backward) => end_head + 1,
84− // if we are appending, the anchor stays where it is; only offset
85− // for multiple range insertions
86− (_, Direction::Forward) => start_range.anchor + offset,
170+ (_, Direction::Forward) => {
171+ if single_grapheme {
172+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head)
173+ + typed_char.len_utf8()
175+ // if we are appending, the anchor stays where it is; only offset
176+ // for multiple range insertions
178+ start_range.anchor + offset
182+ (_, Direction::Backward) => {
183+ if single_grapheme {
184+ // if we're backward, then the head is at the first char
185+ // of the typed char, so we need to add the length of
187+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
88− // when we are inserting in front of a selection, we need to move
189+ // when we are inserting in front of a selection, we need to move
89− // the anchor over by however many characters were inserted overall
190+ // the anchor over by however many characters were inserted overall
90− (_, Direction::Backward) => start_range.anchor + offset + len_inserted,
191+ start_range.anchor + offset + len_inserted
93 Range::new(end_anchor, end_head)
196 Range::new(end_anchor, end_head)
@@ -122,7 +225,7 @@ fn handle_open(
125− let next_range = get_next_range(start_range, offs, open, len_inserted);
228+ let next_range = get_next_range(doc, start_range, offs, open, len_inserted);
126 end_ranges.push(next_range);
229 end_ranges.push(next_range);
127 offs += len_inserted;
230 offs += len_inserted;
@@ -152,7 +255,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
152 (cursor, cursor, Some(Tendril::from_char(close)))
255 (cursor, cursor, Some(Tendril::from_char(close)))
155− let next_range = get_next_range(start_range, offs, close, len_inserted);
258+ let next_range = get_next_range(doc, start_range, offs, close, len_inserted);
156 end_ranges.push(next_range);
259 end_ranges.push(next_range);
157 offs += len_inserted;
260 offs += len_inserted;
@@ -202,7 +305,7 @@ fn handle_same(
202 (cursor, cursor, Some(pair))
305 (cursor, cursor, Some(pair))
205− let next_range = get_next_range(start_range, offs, token, len_inserted);
308+ let next_range = get_next_range(doc, start_range, offs, token, len_inserted);
206 end_ranges.push(next_range);
309 end_ranges.push(next_range);
207 offs += len_inserted;
310 offs += len_inserted;
@@ -219,6 +322,8 @@ mod test {
219 use super::*;
322 use super::*;
220 use smallvec::smallvec;
323 use smallvec::smallvec;
325+ const LINE_END: &'static str = crate::DEFAULT_LINE_ENDING.as_str();
222 fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
327 fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
223 PAIRS.iter().filter(|(open, close)| open != close)
328 PAIRS.iter().filter(|(open, close)| open != close)
@@ -270,12 +375,59 @@ mod test {
271 fn test_insert_blank() {
376 fn test_insert_blank() {
272 test_hooks_with_pairs(
377 test_hooks_with_pairs(
273− &Rope::new(),
378+ &Rope::from(LINE_END),
274 &Selection::single(1, 0),
379 &Selection::single(1, 0),
276− |open, close| format!("{}{}", open, close),
381+ |open, close| format!("{}{}{}", open, close, LINE_END),
277 &Selection::single(2, 1),
382 &Selection::single(2, 1),
385+ let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END));
387+ test_hooks_with_pairs(
389+ &Selection::single(empty_doc.len_chars(), LINE_END.len()),
393+ "{line_end}{open}{close}{line_end}",
399+ &Selection::single(LINE_END.len() + 2, LINE_END.len() + 1),
404+ fn test_insert_before_multi_code_point_graphemes() {
405+ test_hooks_with_pairs(
406+ &Rope::from(format!("hello 👨👩👧👦 goodbye{}", LINE_END)),
407+ &Selection::single(13, 6),
409+ |open, _| format!("hello {}👨👩👧👦 goodbye{}", open, LINE_END),
410+ &Selection::single(14, 7),
415+ fn test_insert_at_end_of_document() {
416+ test_hooks_with_pairs(
417+ &Rope::from(LINE_END),
418+ &Selection::single(LINE_END.len(), LINE_END.len()),
420+ |open, close| format!("{}{}{}", LINE_END, open, close),
421+ &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
424+ test_hooks_with_pairs(
425+ &Rope::from(format!("foo{}", LINE_END)),
426+ &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
428+ |open, close| format!("foo{}{}{}", LINE_END, open, close),
429+ &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
281 /// [] -> append ( -> ([])
433 /// [] -> append ( -> ([])
@@ -283,11 +435,20 @@ mod test {
283 fn test_append_blank() {
435 fn test_append_blank() {
284 test_hooks_with_pairs(
436 test_hooks_with_pairs(
285 // this is what happens when you have a totally blank document and then append
437 // this is what happens when you have a totally blank document and then append
286− &Rope::from("\n\n"),
438+ &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
439+ // before inserting the pair, the cursor covers all of both empty lines
287− &Selection::single(0, 2),
440+ &Selection::single(0, LINE_END.len() * 2),
289− |open, close| format!("\n{}{}\n", open, close),
442+ |open, close| {
444+ "{line_end}{open}{close}{line_end}",
445+ line_end = LINE_END,
450+ // after inserting pair, the cursor covers the first new line and the open char
290− &Selection::single(0, 3),
451+ &Selection::single(0, LINE_END.len() + 2),
@@ -329,6 +490,18 @@ mod test {
493+ /// foo[] -> append to end of line ( -> foo([])
495+ fn test_append_single_cursor() {
496+ test_hooks_with_pairs(
497+ &Rope::from(format!("foo{}", LINE_END)),
498+ &Selection::single(3, 3 + LINE_END.len()),
500+ |open, close| format!("foo{}{}{}", open, close, LINE_END),
501+ &Selection::single(4, 5),
332 /// fo[o] fo[o(])
505 /// fo[o] fo[o(])
333 /// fo[o] -> append ( -> fo[o(])
506 /// fo[o] -> append ( -> fo[o(])
334 /// fo[o] fo[o(])
507 /// fo[o] fo[o(])
@@ -355,18 +528,18 @@ mod test {
358− /// ([]) -> insert ) -> ()[]
531+ /// ([)] -> insert ) -> ()[]
360 fn test_insert_close_inside_pair() {
533 fn test_insert_close_inside_pair() {
361 for (open, close) in PAIRS {
534 for (open, close) in PAIRS {
362− let doc = Rope::from(format!("{}{}", open, close));
535+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
364 test_hooks(
537 test_hooks(
366 &Selection::single(2, 1),
539 &Selection::single(2, 1),
369− &Selection::single(3, 2),
542+ &Selection::single(2 + LINE_END.len(), 2),
@@ -375,14 +548,14 @@ mod test {
376 fn test_append_close_inside_pair() {
549 fn test_append_close_inside_pair() {
377 for (open, close) in PAIRS {
550 for (open, close) in PAIRS {
378− let doc = Rope::from(format!("{}{}\n", open, close));
551+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
380 test_hooks(
553 test_hooks(
382 &Selection::single(0, 2),
555 &Selection::single(0, 2),
385− &Selection::single(0, 3),
558+ &Selection::single(0, 2 + LINE_END.len()),
@@ -564,6 +737,20 @@ mod test {
740+ /// foo([) wor]d -> insert ) -> foo()[ wor]d
742+ fn test_insert_close_inside_pair_trailing_word_with_selection() {
743+ for (open, close) in differing_pairs() {
745+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
746+ &Selection::single(9, 4),
748+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
749+ &Selection::single(9, 5),
567 /// we want pairs that are *not* the same char to be inserted after
754 /// we want pairs that are *not* the same char to be inserted after
568 /// a non-pair char, for cases like functions, but for pairs that are
755 /// a non-pair char, for cases like functions, but for pairs that are
569 /// the same char, we want to *not* insert a pair to handle cases like "I'm"
756 /// the same char, we want to *not* insert a pair to handle cases like "I'm"
@@ -572,7 +759,7 @@ mod test {
572 /// word[] -> insert ' -> word'[]
759 /// word[] -> insert ' -> word'[]
574 fn test_insert_open_after_non_pair() {
761 fn test_insert_open_after_non_pair() {
575− let doc = Rope::from("word");
762+ let doc = Rope::from(format!("word{}", LINE_END));
576 let sel = Selection::single(5, 4);
763 let sel = Selection::single(5, 4);
577 let expected_sel = Selection::single(6, 5);
764 let expected_sel = Selection::single(6, 5);
@@ -580,7 +767,7 @@ mod test {
582 differing_pairs(),
769 differing_pairs(),
583− |open, close| format!("word{}{}", open, close),
770+ |open, close| format!("word{}{}{}", open, close, LINE_END),
584 &expected_sel,
771 &expected_sel,
@@ -588,22 +775,8 @@ mod test {
590 matching_pairs(),
777 matching_pairs(),
591− |open, _| format!("word{}", open),
778+ |open, _| format!("word{}{}", open, LINE_END),
592 &expected_sel,
779 &expected_sel,
596− /// appending with only a cursor should stay a cursor
598− /// [] -> append to end "foo -> "foo[]"
600− fn test_append_single_cursor() {
601− test_hooks_with_pairs(
603− &Selection::single(0, 1),
605− |open, close| format!("{}{}\n", open, close),
606− &Selection::single(1, 2),