diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index c037afef4d72..1b3de6ea08bd 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -1,7 +1,7 @@ //! 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::{Range, Rope, Selection, Tendril, Transaction}; +use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction}; use log::debug; use smallvec::SmallVec; @@ -30,7 +30,6 @@ const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{20 // [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 {% %} @@ -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 { debug!("autopairs hook selection: {:#?}", selection); - let cursors = selection.clone().cursors(doc.slice(..)); - for &(open, close) in PAIRS { if open == ch { 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 { - return Some(handle_open(doc, &cursors, open, close, CLOSE_BEFORE)); + return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE)); } } if close == ch { // && 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 { 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( doc: &Rope, selection: &Selection, @@ -74,36 +101,32 @@ fn handle_open( close_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 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 { - start_range.anchor + offs - }; - - end_ranges.push(Range::new(end_anchor, end_head)); + let cursor = start_range.cursor(doc.slice(..)); + let next_char = doc.get_char(cursor); + let len_inserted; - match next { + let change = match next_char { Some(ch) if !close_before.contains(ch) => { - offs += open.len_utf8(); - (start_head, start_head, Some(Tendril::from_char(open))) + len_inserted = open.len_utf8(); + (cursor, cursor, Some(Tendril::from_char(open))) } // None | Some(ch) if close_before.contains(ch) => {} _ => { // insert open & close let pair = Tendril::from_iter([open, close]); - offs += open.len_utf8() + close.len_utf8(); - (start_head, start_head, Some(pair)) + len_inserted = open.len_utf8() + close.len_utf8(); + (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())); @@ -117,28 +140,28 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> let mut offs = 0; 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 cursor = start_range.cursor(doc.slice(..)); + let next_char = doc.get_char(cursor); + let mut len_inserted = 0; - let end_anchor = if start_range.is_empty() { - end_head + let change = if next_char == Some(close) { + // return transaction that moves past close + (cursor, cursor, None) // no-op } 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) { - // 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))) - } + 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 } /// 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 transaction = Transaction::change_by_selection(doc, selection, |start_range| { - let start_head = start_range.head; - let end_head = start_head + offs + token.len_utf8(); + let cursor = start_range.cursor(doc.slice(..)); + let mut len_inserted = 0; - // if selection, retain anchor, if cursor, move over - let end_anchor = if start_range.is_empty() { - end_head - } else { - start_range.anchor + offs - }; + let next_char = doc.get_char(cursor); + let prev_char = prev_char(doc, cursor); - 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) { + let change = if next_char == Some(token) { // return transaction that moves past close - (start_head, start_head, None) // no-op + (cursor, cursor, 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())) + if (next_char.is_none() || close_before.contains(next_char.unwrap())) + && (prev_char.is_none() || open_before.contains(prev_char.unwrap())) { pair.push_char(token); } - offs += pair.len(); - (start_head, start_head, Some(pair)) - } + len_inserted += pair.len(); + (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)] @@ -252,7 +274,20 @@ mod test { &Selection::single(1, 0), PAIRS, |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( - smallvec!(Range::point(1), Range::point(4), Range::point(7),), + smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 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"), + &Rope::from("foo\n"), &Selection::single(2, 4), - PAIRS, - |open, close| format!("foo{}{}", open, close), + differing_pairs(), + |open, close| format!("foo{}{}\n", open, close), &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 ) -> ()[] #[test] fn test_insert_close_inside_pair() { @@ -307,7 +366,23 @@ mod test { &Selection::single(2, 1), *close, &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( - // smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),), - smallvec!(Range::point(2), Range::point(5), Range::point(8),), + smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 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, ); @@ -343,7 +443,7 @@ mod test { #[test] fn test_insert_open_inside_pair() { 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() { 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 " -> ("[]") #[test] fn test_insert_nested_open_inside_pair() { 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() { let doc = Rope::from(format!("{}{}", outer_open, outer_close,)); @@ -385,21 +523,44 @@ mod test { &Selection::single(1, 0), PAIRS, |open, _| format!("{}word", open), - &Selection::point(1), + &Selection::single(2, 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), + &Selection::single(3, 0), PAIRS, |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() { let doc = Rope::from("word"); let sel = Selection::single(5, 4); - let expected_sel = Selection::point(5); + let expected_sel = Selection::single(6, 5); test_hooks_with_pairs( &doc, @@ -431,4 +592,18 @@ mod test { &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), + ); + } } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 116a1c7c09b2..884c98acf8c3 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -7,6 +7,7 @@ use crate::{ ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, prev_grapheme_boundary, }, + movement::Direction, Assoc, ChangeSet, RopeSlice, }; use smallvec::{smallvec, SmallVec}; @@ -82,6 +83,13 @@ impl Range { 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. #[inline] #[must_use] @@ -102,6 +110,18 @@ impl Range { 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. #[must_use] pub fn overlaps(&self, other: &Self) -> bool {