on master help sign in
fix(auto_pairs): fix auto pairs with crlf (#1470)

Auto pairs were resulting in incorrect ranges in the resulting when the line terminators are CRLF (i.e. Windows). It turns out this is because when we were checking if the selection was a single-width cursor, it incorrectly assumed that this would be a single char. This is not the case, as a cursor can cover a multi-code point grapheme. Therefore, we must instead explicitly work with and check graphemes to determine if the cursor should move or extend the selection. Fixes #1436

commit56a9ce5d8334e82bffb56986cef554c2ac81268f authorSkyler Hawthorne <skyler@dead10ck.com> dateMon 17 Jan 2022, 10:39 (5 years ago) parent8ea5742
1 file changed+214−41
helix-core/src/auto_pairs.rs+214−41
helix-core/src/auto_pairs.rs+214−41
@@ -1,7 +1,9 @@
11//! When typing the opening character of one of the possible pairs defined below,
22//! this module provides the functionality to insert the paired closing character.
33
4−use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction};
4+use crate::{
5+ graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
6+};
57use log::debug;
68use smallvec::SmallVec;
79
@@ -63,31 +65,132 @@ fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
6365 doc.get_char(pos - 1)
6466}
6567
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()
74+}
75+
6676/// calculate what the resulting range should be for an auto pair insertion
6777fn get_next_range(
78+ doc: &Rope,
6879 start_range: &Range,
6980 offset: usize,
7081 typed_char: char,
7182 len_inserted: usize,
7283) -> Range {
73− let end_head = start_range.head + offset + typed_char.len_utf8();
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.
87+ //
88+ // foo[\r\n] - anchor: 3, head: 5
89+ // foo([)]\r\n - anchor: 4, head: 5
90+ //
91+ // foo[\r\n] - anchor: 3, head: 5
92+ // foo'[\r\n] - anchor: 4, head: 6
93+ //
94+ // foo([)]\r\n - anchor: 4, head: 5
95+ // foo()[\r\n] - anchor: 5, head: 7
96+ //
97+ // [foo]\r\n - anchor: 0, head: 3
98+ // [foo(])\r\n - anchor: 0, head: 5
99+
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() {
102+ return Range::new(
103+ start_range.anchor + offset + typed_char.len_utf8(),
104+ start_range.head + offset + typed_char.len_utf8(),
105+ );
106+ }
107+
108+ let single_grapheme = is_single_grapheme(doc, start_range);
109+ let doc_slice = doc.slice(..);
110+
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
115+
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.:
119+ //
120+ // foo([) wor]d -> insert ) -> foo()[ wor]d
121+ } else {
122+ start_range.anchor + offset
123+ };
124+
125+ return Range::new(
126+ end_anchor,
127+ graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
128+ );
129+ }
130+
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()
135+ } else {
136+ start_range.anchor + offset
137+ };
138+
139+ return Range::new(
140+ end_anchor,
141+ start_range.head + offset + typed_char.len_utf8(),
142+ );
143+ }
144+
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 {
148+ start_range.head + offset + typed_char.len_utf8()
149+ } else {
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);
154+ debug!(
155+ "prev_bound: {}, offset: {}, len_inserted: {}",
156+ prev_bound, offset, len_inserted
157+ );
158+ prev_bound + offset + len_inserted
159+ };
74160
75161 let end_anchor = match (start_range.len(), start_range.direction()) {
76162 // if we have a zero width cursor, it shifts to the same number
77163 (0, _) => end_head,
78164
79− // if we are inserting for a regular one-width cursor, the anchor
80− // moves with the head
165+ // If we are inserting for a regular one-width cursor, the anchor
166+ // moves with the head. This is the fast path for ASCII.
81167 (1, Direction::Forward) => end_head - 1,
82168 (1, Direction::Backward) => end_head + 1,
83169
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()
87174
88− // when we are inserting in front of a selection, we need to move
89− // the anchor over by however many characters were inserted overall
90− (_, Direction::Backward) => start_range.anchor + offset + len_inserted,
175+ // if we are appending, the anchor stays where it is; only offset
176+ // for multiple range insertions
177+ } else {
178+ start_range.anchor + offset
179+ }
180+ }
181+
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
186+ // the closing char
187+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
188+ } else {
189+ // when we are inserting in front of a selection, we need to move
190+ // the anchor over by however many characters were inserted overall
191+ start_range.anchor + offset + len_inserted
192+ }
193+ }
91194 };
92195
93196 Range::new(end_anchor, end_head)
@@ -122,7 +225,7 @@ fn handle_open(
122225 }
123226 };
124227
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);
126229 end_ranges.push(next_range);
127230 offs += len_inserted;
128231
@@ -152,7 +255,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
152255 (cursor, cursor, Some(Tendril::from_char(close)))
153256 };
154257
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);
156259 end_ranges.push(next_range);
157260 offs += len_inserted;
158261
@@ -202,7 +305,7 @@ fn handle_same(
202305 (cursor, cursor, Some(pair))
203306 };
204307
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);
206309 end_ranges.push(next_range);
207310 offs += len_inserted;
208311
@@ -219,6 +322,8 @@ mod test {
219322 use super::*;
220323 use smallvec::smallvec;
221324
325+ const LINE_END: &'static str = crate::DEFAULT_LINE_ENDING.as_str();
326+
222327 fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
223328 PAIRS.iter().filter(|(open, close)| open != close)
224329 }
@@ -270,12 +375,59 @@ mod test {
270375 #[test]
271376 fn test_insert_blank() {
272377 test_hooks_with_pairs(
273− &Rope::new(),
378+ &Rope::from(LINE_END),
274379 &Selection::single(1, 0),
275380 PAIRS,
276− |open, close| format!("{}{}", open, close),
381+ |open, close| format!("{}{}{}", open, close, LINE_END),
277382 &Selection::single(2, 1),
278383 );
384+
385+ let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END));
386+
387+ test_hooks_with_pairs(
388+ &empty_doc,
389+ &Selection::single(empty_doc.len_chars(), LINE_END.len()),
390+ PAIRS,
391+ |open, close| {
392+ format!(
393+ "{line_end}{open}{close}{line_end}",
394+ open = open,
395+ close = close,
396+ line_end = LINE_END
397+ )
398+ },
399+ &Selection::single(LINE_END.len() + 2, LINE_END.len() + 1),
400+ );
401+ }
402+
403+ #[test]
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),
408+ PAIRS,
409+ |open, _| format!("hello {}👨‍👩‍👧‍👦 goodbye{}", open, LINE_END),
410+ &Selection::single(14, 7),
411+ );
412+ }
413+
414+ #[test]
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()),
419+ PAIRS,
420+ |open, close| format!("{}{}{}", LINE_END, open, close),
421+ &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
422+ );
423+
424+ test_hooks_with_pairs(
425+ &Rope::from(format!("foo{}", LINE_END)),
426+ &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
427+ PAIRS,
428+ |open, close| format!("foo{}{}{}", LINE_END, open, close),
429+ &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
430+ );
279431 }
280432
281433 /// [] -> append ( -> ([])
@@ -283,11 +435,20 @@ mod test {
283435 fn test_append_blank() {
284436 test_hooks_with_pairs(
285437 // this is what happens when you have a totally blank document and then append
286− &Rope::from("\n\n"),
287− &Selection::single(0, 2),
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
440+ &Selection::single(0, LINE_END.len() * 2),
288441 PAIRS,
289− |open, close| format!("\n{}{}\n", open, close),
290− &Selection::single(0, 3),
442+ |open, close| {
443+ format!(
444+ "{line_end}{open}{close}{line_end}",
445+ line_end = LINE_END,
446+ open = open,
447+ close = close
448+ )
449+ },
450+ // after inserting pair, the cursor covers the first new line and the open char
451+ &Selection::single(0, LINE_END.len() + 2),
291452 );
292453 }
293454
@@ -329,6 +490,18 @@ mod test {
329490 );
330491 }
331492
493+ /// foo[] -> append to end of line ( -> foo([])
494+ #[test]
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()),
499+ differing_pairs(),
500+ |open, close| format!("foo{}{}{}", open, close, LINE_END),
501+ &Selection::single(4, 5),
502+ );
503+ }
504+
332505 /// fo[o] fo[o(])
333506 /// fo[o] -> append ( -> fo[o(])
334507 /// fo[o] fo[o(])
@@ -355,18 +528,18 @@ mod test {
355528 );
356529 }
357530
358− /// ([]) -> insert ) -> ()[]
531+ /// ([)] -> insert ) -> ()[]
359532 #[test]
360533 fn test_insert_close_inside_pair() {
361534 for (open, close) in PAIRS {
362− let doc = Rope::from(format!("{}{}", open, close));
535+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
363536
364537 test_hooks(
365538 &doc,
366539 &Selection::single(2, 1),
367540 *close,
368541 &doc,
369− &Selection::single(3, 2),
542+ &Selection::single(2 + LINE_END.len(), 2),
370543 );
371544 }
372545 }
@@ -375,14 +548,14 @@ mod test {
375548 #[test]
376549 fn test_append_close_inside_pair() {
377550 for (open, close) in PAIRS {
378− let doc = Rope::from(format!("{}{}\n", open, close));
551+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
379552
380553 test_hooks(
381554 &doc,
382555 &Selection::single(0, 2),
383556 *close,
384557 &doc,
385− &Selection::single(0, 3),
558+ &Selection::single(0, 2 + LINE_END.len()),
386559 );
387560 }
388561 }
@@ -564,6 +737,20 @@ mod test {
564737 )
565738 }
566739
740+ /// foo([) wor]d -> insert ) -> foo()[ wor]d
741+ #[test]
742+ fn test_insert_close_inside_pair_trailing_word_with_selection() {
743+ for (open, close) in differing_pairs() {
744+ test_hooks(
745+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
746+ &Selection::single(9, 4),
747+ *close,
748+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
749+ &Selection::single(9, 5),
750+ )
751+ }
752+ }
753+
567754 /// we want pairs that are *not* the same char to be inserted after
568755 /// a non-pair char, for cases like functions, but for pairs that are
569756 /// the same char, we want to *not* insert a pair to handle cases like "I'm"
@@ -572,7 +759,7 @@ mod test {
572759 /// word[] -> insert ' -> word'[]
573760 #[test]
574761 fn test_insert_open_after_non_pair() {
575− let doc = Rope::from("word");
762+ let doc = Rope::from(format!("word{}", LINE_END));
576763 let sel = Selection::single(5, 4);
577764 let expected_sel = Selection::single(6, 5);
578765
@@ -580,7 +767,7 @@ mod test {
580767 &doc,
581768 &sel,
582769 differing_pairs(),
583− |open, close| format!("word{}{}", open, close),
770+ |open, close| format!("word{}{}{}", open, close, LINE_END),
584771 &expected_sel,
585772 );
586773
@@ -588,22 +775,8 @@ mod test {
588775 &doc,
589776 &sel,
590777 matching_pairs(),
591− |open, _| format!("word{}", open),
778+ |open, _| format!("word{}{}", open, LINE_END),
592779 &expected_sel,
593780 );
594781 }
595−
596− /// appending with only a cursor should stay a cursor
597− ///
598− /// [] -> append to end "foo -> "foo[]"
599− #[test]
600− fn test_append_single_cursor() {
601− test_hooks_with_pairs(
602− &Rope::from("\n"),
603− &Selection::single(0, 1),
604− PAIRS,
605− |open, close| format!("{}{}\n", open, close),
606− &Selection::single(1, 2),
607− );
608− }
609782}