diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index eff1fcd70..a382a7186 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -184,16 +184,16 @@ impl Range { let positions_to_map = match self.anchor.cmp(&self.head) { Ordering::Equal => [ - (&mut self.anchor, Assoc::After), - (&mut self.head, Assoc::After), + (&mut self.anchor, Assoc::AfterSticky), + (&mut self.head, Assoc::AfterSticky), ], Ordering::Less => [ - (&mut self.anchor, Assoc::After), - (&mut self.head, Assoc::Before), + (&mut self.anchor, Assoc::AfterSticky), + (&mut self.head, Assoc::BeforeSticky), ], Ordering::Greater => [ - (&mut self.head, Assoc::After), - (&mut self.anchor, Assoc::Before), + (&mut self.head, Assoc::AfterSticky), + (&mut self.anchor, Assoc::BeforeSticky), ], }; changes.update_positions(positions_to_map.into_iter()); @@ -482,16 +482,16 @@ impl Selection { range.old_visual_position = None; match range.anchor.cmp(&range.head) { Ordering::Equal => [ - (&mut range.anchor, Assoc::After), - (&mut range.head, Assoc::After), + (&mut range.anchor, Assoc::AfterSticky), + (&mut range.head, Assoc::AfterSticky), ], Ordering::Less => [ - (&mut range.anchor, Assoc::After), - (&mut range.head, Assoc::Before), + (&mut range.anchor, Assoc::AfterSticky), + (&mut range.head, Assoc::BeforeSticky), ], Ordering::Greater => [ - (&mut range.head, Assoc::After), - (&mut range.anchor, Assoc::Before), + (&mut range.head, Assoc::AfterSticky), + (&mut range.anchor, Assoc::BeforeSticky), ], } }); diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index f24f20942..c5c94b750 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -29,6 +29,12 @@ pub enum Assoc { /// Acts like `Before` if a word character is inserted /// before the position, otherwise acts like `After` BeforeWord, + /// Acts like `Before` but if the position is within an exact replacement + /// (exact size) the offset to the start of the replacement is kept + BeforeSticky, + /// Acts like `After` but if the position is within an exact replacement + /// (exact size) the offset to the start of the replacement is kept + AfterSticky, } impl Assoc { @@ -40,13 +46,17 @@ impl Assoc { fn insert_offset(self, s: &str) -> usize { let chars = s.chars().count(); match self { - Assoc::After => chars, + Assoc::After | Assoc::AfterSticky => chars, Assoc::AfterWord => s.chars().take_while(|&c| char_is_word(c)).count(), // return position before inserted text - Assoc::Before => 0, + Assoc::Before | Assoc::BeforeSticky => 0, Assoc::BeforeWord => chars - s.chars().rev().take_while(|&c| char_is_word(c)).count(), } } + + pub fn sticky(self) -> bool { + matches!(self, Assoc::BeforeSticky | Assoc::AfterSticky) + } } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -456,8 +466,14 @@ impl ChangeSet { if pos == old_pos && assoc.stay_at_gaps() { new_pos } else { - // place to end of insert - new_pos + assoc.insert_offset(s) + let ins = assoc.insert_offset(s); + // if the deleted and inserted text have the exact same size + // keep the relative offset into the new text + if *len == ins && assoc.sticky() { + new_pos + (pos - old_pos) + } else { + new_pos + assoc.insert_offset(s) + } } }), i