aboutsummaryrefslogtreecommitdiff
path: root/helix-core/src/transaction.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core/src/transaction.rs')
-rw-r--r--helix-core/src/transaction.rs87
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]