summaryrefslogtreecommitdiff
path: root/helix-core
diff options
context:
space:
mode:
Diffstat (limited to 'helix-core')
-rw-r--r--helix-core/src/transaction.rs126
1 files changed, 123 insertions, 3 deletions
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index 60ccd7c6..c6be89ac 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -39,6 +39,12 @@ impl Change {
}
}
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Assoc {
+ Before,
+ After,
+}
+
// ChangeSpec = Change | ChangeSet | Vec<Change>
// ChangeDesc as a ChangeSet without text: can't be applied, cheaper to store.
// ChangeSet = ChangeDesc with Text
@@ -236,7 +242,82 @@ impl ChangeSet {
}
}
- // iter over changes
+ /// Map a position through the changes.
+ ///
+ /// `assoc` indicates which size to associate the position with. `Before` will keep the
+ /// position close to the character before, and will place it before insertions over that
+ /// range, or at that point. `After` will move it forward, placing it at the end of such
+ /// insertions.
+ pub fn map_pos(&self, pos: usize, assoc: Assoc) -> usize {
+ use Change::*;
+ let mut old_pos = 0;
+ let mut new_pos = 0;
+
+ let mut iter = self.changes.iter().peekable();
+
+ while let Some(change) = iter.next() {
+ let len = match change {
+ Delete(i) | Retain(i) => *i,
+ Insert(_) => 0,
+ };
+ let old_end = old_pos + len;
+
+ match change {
+ Retain(_len) => {
+ if old_end > pos {
+ return new_pos + (pos - old_pos);
+ }
+ new_pos += len;
+ }
+ Delete(_len) => {
+ // a subsequent ins means a replace, consume it
+ let ins = if let Some(Insert(s)) = iter.peek() {
+ iter.next();
+ s.chars().count()
+ } else {
+ 0
+ };
+
+ // in range
+ if old_end > pos {
+ // at point or tracking before
+ if pos == old_pos || assoc == Assoc::Before {
+ return new_pos;
+ } else {
+ // place to end of delete
+ return new_pos + ins;
+ }
+ }
+
+ new_pos += ins;
+ }
+ Insert(s) => {
+ let ins = s.chars().count();
+ // at insert point
+ if old_pos == pos {
+ // return position before inserted text
+ if assoc == Assoc::Before {
+ return new_pos;
+ } else {
+ // after text
+ return new_pos + ins;
+ }
+ }
+
+ new_pos += ins;
+ }
+ }
+ old_pos = old_end;
+ }
+
+ if pos > old_pos {
+ panic!(
+ "Position {} is out of range for changeset len {}!",
+ pos, old_pos
+ )
+ }
+ new_pos
+ }
}
// trait Transaction
@@ -282,9 +363,48 @@ mod test {
// should probably return cloned text
a.compose(b).unwrap().apply(&mut text);
- unimplemented!("{:?}", text);
+ // unimplemented!("{:?}", text);
+ // TODO: assert
}
#[test]
- fn map() {}
+ fn map_pos() {
+ use Change::*;
+
+ // maps inserts
+ let cs = ChangeSet {
+ changes: vec![Retain(4), Insert("!!".into()), Retain(4)],
+ len: 8,
+ };
+
+ assert_eq!(cs.map_pos(0, Assoc::Before), 0); // before insert region
+ assert_eq!(cs.map_pos(4, Assoc::Before), 4); // at insert, track before
+ assert_eq!(cs.map_pos(4, Assoc::After), 6); // at insert, track after
+ assert_eq!(cs.map_pos(5, Assoc::Before), 7); // after insert region
+
+ // maps deletes
+ let cs = ChangeSet {
+ changes: vec![Retain(4), Delete(4), Retain(4)],
+ len: 12,
+ };
+ assert_eq!(cs.map_pos(0, Assoc::Before), 0); // at start
+ assert_eq!(cs.map_pos(4, Assoc::Before), 4); // before a delete
+ assert_eq!(cs.map_pos(5, Assoc::Before), 4); // inside a delete
+ assert_eq!(cs.map_pos(5, Assoc::After), 4); // inside a delete
+
+ // TODO: delete tracking
+
+ // stays inbetween replacements
+ let cs = ChangeSet {
+ changes: vec![
+ Delete(2),
+ Insert("ab".into()),
+ Delete(2),
+ Insert("cd".into()),
+ ],
+ len: 4,
+ };
+ assert_eq!(cs.map_pos(2, Assoc::Before), 2);
+ assert_eq!(cs.map_pos(2, Assoc::After), 2);
+ }
}