diff options
Diffstat (limited to 'helix-core/src/transaction.rs')
-rw-r--r-- | helix-core/src/transaction.rs | 87 |
1 files changed, 73 insertions, 14 deletions
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 1cea6911..f5a49cc1 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -1,7 +1,7 @@ use ropey::RopeSlice; use smallvec::SmallVec; -use crate::{Range, Rope, Selection, Tendril}; +use crate::{chars::char_is_word, Range, Rope, Selection, Tendril}; use std::{borrow::Cow, iter::once}; /// (from, to, replacement) @@ -23,6 +23,30 @@ pub enum Operation { pub enum Assoc { Before, After, + /// Acts like `After` if a word character is inserted + /// after the position, otherwise acts like `Before` + AfterWord, + /// Acts like `Before` if a word character is inserted + /// before the position, otherwise acts like `After` + BeforeWord, +} + +impl Assoc { + /// Whether to stick to gaps + fn stay_at_gaps(self) -> bool { + !matches!(self, Self::BeforeWord | Self::AfterWord) + } + + fn insert_offset(self, s: &str) -> usize { + let chars = s.chars().count(); + match self { + Assoc::After => chars, + Assoc::AfterWord => s.chars().take_while(|&c| char_is_word(c)).count(), + // return position before inserted text + Assoc::Before => 0, + Assoc::BeforeWord => chars - s.chars().rev().take_while(|&c| char_is_word(c)).count(), + } + } } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -415,8 +439,6 @@ impl ChangeSet { map!(|pos, _| (old_end > pos).then_some(new_pos), i); } Insert(s) => { - let ins = s.chars().count(); - // a subsequent delete means a replace, consume it if let Some((_, Delete(len))) = iter.peek() { iter.next(); @@ -424,13 +446,13 @@ impl ChangeSet { old_end = old_pos + len; // in range of replaced text map!( - |pos, assoc| (old_end > pos).then(|| { + |pos, assoc: Assoc| (old_end > pos).then(|| { // at point or tracking before - if pos == old_pos || assoc == Assoc::Before { + if pos == old_pos && assoc.stay_at_gaps() { new_pos } else { // place to end of insert - new_pos + ins + new_pos + assoc.insert_offset(s) } }), i @@ -438,20 +460,15 @@ impl ChangeSet { } else { // at insert point map!( - |pos, assoc| (old_pos == pos).then(|| { + |pos, assoc: Assoc| (old_pos == pos).then(|| { // return position before inserted text - if assoc == Assoc::Before { - new_pos - } else { - // after text - new_pos + ins - } + new_pos + assoc.insert_offset(s) }), i ); } - new_pos += ins; + new_pos += s.chars().count(); } } old_pos = old_end; @@ -884,6 +901,48 @@ mod test { let mut positions = [4, 2]; cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After))); assert_eq!(positions, [4, 2]); + // stays at word boundary + let cs = ChangeSet { + changes: vec![ + Retain(2), // <space><space> + Insert(" ab".into()), + Retain(2), // cd + Insert("de ".into()), + ], + len: 4, + len_after: 10, + }; + assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 3); + assert_eq!(cs.map_pos(4, Assoc::AfterWord), 9); + let cs = ChangeSet { + changes: vec![ + Retain(1), // <space> + Insert(" b".into()), + Delete(1), // c + Retain(1), // d + Insert("e ".into()), + Delete(1), // <space> + ], + len: 5, + len_after: 7, + }; + assert_eq!(cs.map_pos(1, Assoc::BeforeWord), 2); + assert_eq!(cs.map_pos(3, Assoc::AfterWord), 5); + let cs = ChangeSet { + changes: vec![ + Retain(1), // <space> + Insert("a".into()), + Delete(2), // <space>b + Retain(1), // d + Insert("e".into()), + Delete(1), // f + Retain(1), // <space> + ], + len: 5, + len_after: 7, + }; + assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 1); + assert_eq!(cs.map_pos(4, Assoc::AfterWord), 4); } #[test] |