Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/transaction.rs')
| -rw-r--r-- | helix-core/src/transaction.rs | 403 |
1 files changed, 54 insertions, 349 deletions
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index 37be2e2e..482fd6d9 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -1,12 +1,8 @@ -use ropey::RopeSlice; -use smallvec::SmallVec; - -use crate::{chars::char_is_word, Range, Rope, Selection, Tendril}; -use std::{borrow::Cow, iter::once}; +use crate::{Range, Rope, Selection, Tendril}; +use std::borrow::Cow; /// (from, to, replacement) pub type Change = (usize, usize, Option<Tendril>); -pub type Deletion = (usize, usize); // TODO: pub(crate) #[derive(Debug, Clone, PartialEq, Eq)] @@ -19,54 +15,10 @@ pub enum Operation { Insert(Tendril), } -impl Operation { - /// The number of characters affected by the operation. - pub fn len_chars(&self) -> usize { - match self { - Self::Retain(n) | Self::Delete(n) => *n, - Self::Insert(s) => s.chars().count(), - } - } -} - #[derive(Debug, Copy, Clone, PartialEq, Eq)] 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, - /// 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 { - /// 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 | Assoc::AfterSticky => chars, - Assoc::AfterWord => s.chars().take_while(|&c| char_is_word(c)).count(), - // return position before inserted text - 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)] @@ -87,7 +39,7 @@ impl ChangeSet { } #[must_use] - pub fn new(doc: RopeSlice) -> Self { + pub fn new(doc: &Rope) -> Self { let len = doc.len_chars(); Self { changes: Vec::new(), @@ -361,7 +313,6 @@ impl ChangeSet { pos += s.chars().count(); } } - println!("=>\n{text}"); } true } @@ -372,80 +323,20 @@ impl ChangeSet { self.changes.is_empty() || self.changes == [Operation::Retain(self.len)] } - /// Map a (mostly) *sorted* list of positions through the changes. - /// - /// This is equivalent to updating each position with `map_pos`: + /// Map a position through the changes. /// - /// ``` no-compile - /// for (pos, assoc) in positions { - /// *pos = changes.map_pos(*pos, assoc); - /// } - /// ``` - /// However this function is significantly faster for sorted lists running - /// in `O(N+M)` instead of `O(NM)`. This function also handles unsorted/ - /// partially sorted lists. However, in that case worst case complexity is - /// again `O(MN)`. For lists that are often/mostly sorted (like the end of diagnostic ranges) - /// performance is usally close to `O(N + M)` - pub fn update_positions<'a>(&self, positions: impl Iterator<Item = (&'a mut usize, Assoc)>) { + /// `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 Operation::*; - - let mut positions = positions.peekable(); - let mut old_pos = 0; let mut new_pos = 0; - let mut iter = self.changes.iter().enumerate().peekable(); - - 'outer: loop { - macro_rules! map { - ($map: expr, $i: expr) => { - loop { - let Some((pos, assoc)) = positions.peek_mut() else { - return; - }; - if **pos < old_pos { - // Positions are not sorted, revert to the last Operation that - // contains this position and continue iterating from there. - // We can unwrap here since `pos` can not be negative - // (unsigned integer) and iterating backwards to the start - // should always move us back to the start - for (i, change) in self.changes[..$i].iter().enumerate().rev() { - match change { - Retain(i) => { - old_pos -= i; - new_pos -= i; - } - Delete(i) => { - old_pos -= i; - } - Insert(ins) => { - new_pos -= ins.chars().count(); - } - } - if old_pos <= **pos { - iter = self.changes[i..].iter().enumerate().peekable(); - } - } - debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works"); - continue 'outer; - } - #[allow(clippy::redundant_closure_call)] - let Some(new_pos) = $map(**pos, *assoc) else { - break; - }; - **pos = new_pos; - positions.next(); - } - }; - } - let Some((i, change)) = iter.next() else { - map!( - |pos, _| (old_pos == pos).then_some(new_pos), - self.changes.len() - ); - break; - }; + let mut iter = self.changes.iter().peekable(); + while let Some(change) = iter.next() { let len = match change { Delete(i) | Retain(i) => *i, Insert(_) => 0, @@ -454,74 +345,64 @@ impl ChangeSet { match change { Retain(_) => { - map!( - |pos, _| (old_end > pos).then_some(new_pos + (pos - old_pos)), - i - ); + if old_end > pos { + return new_pos + (pos - old_pos); + } new_pos += len; } Delete(_) => { // in range - map!(|pos, _| (old_end > pos).then_some(new_pos), i); + if old_end > pos { + return new_pos; + } } Insert(s) => { + let ins = s.chars().count(); + // a subsequent delete means a replace, consume it - if let Some((_, Delete(len))) = iter.peek() { + if let Some(Delete(len)) = iter.peek() { iter.next(); old_end = old_pos + len; // in range of replaced text - map!( - |pos, assoc: Assoc| (old_end > pos).then(|| { - // at point or tracking before - if pos == old_pos && assoc.stay_at_gaps() { - new_pos - } else { - 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 - ); + if old_end > pos { + // at point or tracking before + if pos == old_pos || assoc == Assoc::Before { + return new_pos; + } else { + // place to end of insert + return new_pos + ins; + } + } } else { // at insert point - map!( - |pos, assoc: Assoc| (old_pos == pos).then(|| { - // return position before inserted text - new_pos + assoc.insert_offset(s) - }), - i - ); + 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 += s.chars().count(); + new_pos += ins; } } old_pos = old_end; } - let out_of_bounds: Vec<_> = positions.collect(); - panic!("Positions {out_of_bounds:?} are out of range for changeset len {old_pos}!",) - } - - /// Map a position through the changes. - /// - /// `assoc` indicates which side 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, mut pos: usize, assoc: Assoc) -> usize { - self.update_positions(once((&mut pos, assoc))); - pos + if pos > old_pos { + panic!( + "Position {} is out of range for changeset len {}!", + pos, old_pos + ) + } + new_pos } - pub fn changes_iter(&self) -> ChangeIterator<'_> { + pub fn changes_iter(&self) -> ChangeIterator { ChangeIterator::new(self) } } @@ -538,7 +419,7 @@ impl Transaction { /// Create a new, empty transaction. pub fn new(doc: &Rope) -> Self { Self { - changes: ChangeSet::new(doc.slice(..)), + changes: ChangeSet::new(doc), selection: None, } } @@ -585,33 +466,6 @@ impl Transaction { self } - /// Generate a transaction from a set of potentially overlapping changes. The `change_ranges` - /// iterator yield the range (of removed text) in the old document for each edit. If any change - /// overlaps with a range overlaps with a previous range then that range is ignored. - /// - /// The `process_change` callback is called for each edit that is not ignored (in the order - /// yielded by `changes`) and should return the new text that the associated range will be - /// replaced with. - /// - /// To make this function more flexible the iterator can yield additional data for each change - /// that is passed to `process_change` - pub fn change_ignore_overlapping<T>( - doc: &Rope, - change_ranges: impl Iterator<Item = (usize, usize, T)>, - mut process_change: impl FnMut(usize, usize, T) -> Option<Tendril>, - ) -> Self { - let mut last = 0; - let changes = change_ranges.filter_map(|(from, to, data)| { - if from < last { - return None; - } - let tendril = process_change(from, to, data); - last = to; - Some((from, to, tendril)) - }); - Self::change(doc, changes) - } - /// Generate a transaction from a set of changes. pub fn change<I>(doc: &Rope, changes: I) -> Self where @@ -627,11 +481,6 @@ impl Transaction { for (from, to, tendril) in changes { // Verify ranges are ordered and not overlapping debug_assert!(last <= from); - // Verify ranges are correct - debug_assert!( - from <= to, - "Edit end must end before it starts (should {from} <= {to})" - ); // Retain from last "to" to current "from" changeset.retain(from - last); @@ -651,46 +500,6 @@ impl Transaction { Self::from(changeset) } - /// Generate a transaction from a set of potentially overlapping deletions - /// by merging overlapping deletions together. - pub fn delete<I>(doc: &Rope, deletions: I) -> Self - where - I: Iterator<Item = Deletion>, - { - let len = doc.len_chars(); - - let (lower, upper) = deletions.size_hint(); - let size = upper.unwrap_or(lower); - let mut changeset = ChangeSet::with_capacity(2 * size + 1); // rough estimate - - let mut last = 0; - for (mut from, to) in deletions { - if last > to { - continue; - } - if last > from { - from = last - } - debug_assert!( - from <= to, - "Edit end must end before it starts (should {from} <= {to})" - ); - // Retain from last "to" to current "from" - changeset.retain(from - last); - changeset.delete(to - from); - last = to; - } - - changeset.retain(len - last); - - Self::from(changeset) - } - - pub fn insert_at_eof(mut self, text: Tendril) -> Transaction { - self.changes.insert(text); - self - } - /// Generate a transaction with a change per selection range. pub fn change_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self where @@ -699,54 +508,6 @@ impl Transaction { Self::change(doc, selection.iter().map(f)) } - pub fn change_by_selection_ignore_overlapping( - doc: &Rope, - selection: &Selection, - mut change_range: impl FnMut(&Range) -> (usize, usize), - mut create_tendril: impl FnMut(usize, usize) -> Option<Tendril>, - ) -> (Transaction, Selection) { - let mut last_selection_idx = None; - let mut new_primary_idx = None; - let mut ranges: SmallVec<[Range; 1]> = SmallVec::new(); - let process_change = |change_start, change_end, (idx, range): (usize, &Range)| { - // update the primary idx - if idx == selection.primary_index() { - new_primary_idx = Some(idx); - } else if new_primary_idx.is_none() { - if idx > selection.primary_index() { - new_primary_idx = last_selection_idx; - } else { - last_selection_idx = Some(idx); - } - } - ranges.push(*range); - create_tendril(change_start, change_end) - }; - let transaction = Self::change_ignore_overlapping( - doc, - selection.iter().enumerate().map(|range| { - let (change_start, change_end) = change_range(range.1); - (change_start, change_end, range) - }), - process_change, - ); - - ( - transaction, - Selection::new(ranges, new_primary_idx.unwrap_or(0)), - ) - } - - /// Generate a transaction with a deletion per selection range. - /// Compared to using `change_by_selection` directly these ranges may overlap. - /// In that case they are merged - pub fn delete_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self - where - F: FnMut(&Range) -> Deletion, - { - Self::delete(doc, selection.iter().map(f)) - } - /// Insert text at each selection head. pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self { Self::change_by_selection(doc, selection, |range| { @@ -754,7 +515,7 @@ impl Transaction { }) } - pub fn changes_iter(&self) -> ChangeIterator<'_> { + pub fn changes_iter(&self) -> ChangeIterator { self.changes.changes_iter() } } @@ -780,7 +541,7 @@ impl<'a> ChangeIterator<'a> { } } -impl Iterator for ChangeIterator<'_> { +impl<'a> Iterator for ChangeIterator<'a> { type Item = Change; fn next(&mut self) -> Option<Self::Item> { @@ -919,62 +680,6 @@ mod test { }; assert_eq!(cs.map_pos(2, Assoc::Before), 2); assert_eq!(cs.map_pos(2, Assoc::After), 2); - // unsorted selection - let cs = ChangeSet { - changes: vec![ - Insert("ab".into()), - Delete(2), - Insert("cd".into()), - Delete(2), - ], - len: 4, - len_after: 4, - }; - 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] @@ -1041,9 +746,9 @@ mod test { #[test] fn combine_with_empty() { let empty = Rope::from(""); - let a = ChangeSet::new(empty.slice(..)); + let a = ChangeSet::new(&empty); - let mut b = ChangeSet::new(empty.slice(..)); + let mut b = ChangeSet::new(&empty); b.insert("a".into()); let changes = a.compose(b); @@ -1057,9 +762,9 @@ mod test { const TEST_CASE: &str = "Hello, これはヘリックスエディターです!"; let empty = Rope::from(""); - let a = ChangeSet::new(empty.slice(..)); + let a = ChangeSet::new(&empty); - let mut b = ChangeSet::new(empty.slice(..)); + let mut b = ChangeSet::new(&empty); b.insert(TEST_CASE.into()); let changes = a.compose(b); |