From 7e628d78d84ee25c116520ae9c6d53014040e7b9 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Thu, 25 Feb 2021 22:13:43 +0100 Subject: [PATCH] Add compaction support (#22) --- Cargo.toml | 4 + examples/patience.rs | 48 ++++++ src/algorithms/compact.rs | 355 ++++++++++++++++++++++++++++++++++++++ src/algorithms/mod.rs | 5 +- src/algorithms/myers.rs | 2 +- src/algorithms/utils.rs | 191 +++++++++++++++++++- src/common.rs | 14 +- src/text/mod.rs | 30 +++- src/types.rs | 82 +++++++++ 9 files changed, 712 insertions(+), 19 deletions(-) create mode 100644 examples/patience.rs create mode 100644 src/algorithms/compact.rs diff --git a/Cargo.toml b/Cargo.toml index a186610..3cf7d12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,10 @@ bstr = { version = "0.2.14", optional = true, default-features = false } name = "assert" required-features = ["text", "inline"] +[[example]] +name = "patience" +required-features = ["text", "inline"] + [[example]] name = "terminal" required-features = ["text"] diff --git a/examples/patience.rs b/examples/patience.rs new file mode 100644 index 0000000..4b898a6 --- /dev/null +++ b/examples/patience.rs @@ -0,0 +1,48 @@ +use similar::{Algorithm, TextDiff}; + +const OLD: &str = r#" +[ + ( + Major, + 2, + ), + ( + Minor, + 20, + ), + ( + Value, + 0, + ), +] +"#; +const NEW: &str = r#" +[ + ( + Major, + 2, + ), + ( + Minor, + 0, + ), + ( + Value, + 0, + ), + ( + Value, + 1, + ), +] +"#; + +fn main() { + println!( + "{}", + TextDiff::configure() + .algorithm(Algorithm::Patience) + .diff_lines(OLD, NEW) + .unified_diff() + ); +} diff --git a/src/algorithms/compact.rs b/src/algorithms/compact.rs new file mode 100644 index 0000000..bd5c6c4 --- /dev/null +++ b/src/algorithms/compact.rs @@ -0,0 +1,355 @@ +//! Implements basic compacting. This is based on the compaction logic from +//! diffy by Brandon Williams. +use std::ops::Index; + +use crate::{DiffOp, DiffTag}; + +use super::utils::{common_prefix_len, common_suffix_len}; +use super::DiffHook; + +/// Performs semantic cleanup operations on a diff. +/// +/// This merges similar ops together but also tries to move hunks up and +/// down the diff with the desire to connect as many hunks as possible. +/// It still needs to be combined with [`Replace`](crate::algorithms::Replace) +/// to get actual replace diff ops out. +#[derive(Debug)] +pub struct Compact<'old, 'new, Old: ?Sized, New: ?Sized, D> { + d: D, + ops: Vec, + old: &'old Old, + new: &'new New, +} + +impl<'old, 'new, Old, New, D> Compact<'old, 'new, Old, New, D> +where + D: DiffHook, + Old: Index + ?Sized + 'old, + New: Index + ?Sized + 'new, + New::Output: PartialEq, +{ + /// Creates a new compact hook wrapping another hook. + pub fn new(d: D, old: &'old Old, new: &'new New) -> Self { + Compact { + d, + ops: Vec::new(), + old, + new, + } + } + + /// Extracts the inner hook. + pub fn into_inner(self) -> D { + self.d + } +} + +impl<'old, 'new, Old: ?Sized, New: ?Sized, D: DiffHook> AsRef + for Compact<'old, 'new, Old, New, D> +{ + fn as_ref(&self) -> &D { + &self.d + } +} + +impl<'old, 'new, Old: ?Sized, New: ?Sized, D: DiffHook> AsMut + for Compact<'old, 'new, Old, New, D> +{ + fn as_mut(&mut self) -> &mut D { + &mut self.d + } +} + +impl<'old, 'new, Old, New, D> DiffHook for Compact<'old, 'new, Old, New, D> +where + D: DiffHook, + Old: Index + ?Sized + 'old, + New: Index + ?Sized + 'new, + New::Output: PartialEq, +{ + type Error = D::Error; + + #[inline(always)] + fn equal(&mut self, old_index: usize, new_index: usize, len: usize) -> Result<(), Self::Error> { + self.ops.push(DiffOp::Equal { + old_index, + new_index, + len, + }); + Ok(()) + } + + #[inline(always)] + fn delete( + &mut self, + old_index: usize, + old_len: usize, + new_index: usize, + ) -> Result<(), Self::Error> { + self.ops.push(DiffOp::Delete { + old_index, + old_len, + new_index, + }); + Ok(()) + } + + #[inline(always)] + fn insert( + &mut self, + old_index: usize, + new_index: usize, + new_len: usize, + ) -> Result<(), Self::Error> { + self.ops.push(DiffOp::Insert { + old_index, + new_index, + new_len, + }); + Ok(()) + } + + fn finish(&mut self) -> Result<(), Self::Error> { + cleanup_diff_ops(self.old, self.new, &mut self.ops); + for op in &self.ops { + op.apply_to_hook(&mut self.d)?; + } + self.d.finish() + } +} + +// Walks through all edits and shifts them up and then down, trying to see if +// they run into similar edits which can be merged. +pub fn cleanup_diff_ops(old: &Old, new: &New, ops: &mut Vec) +where + Old: Index + ?Sized, + New: Index + ?Sized, + New::Output: PartialEq, +{ + // First attempt to compact all Deletions + let mut pointer = 0; + while let Some(&op) = ops.get(pointer) { + if let DiffTag::Delete = op.tag() { + pointer = shift_diff_ops_up(ops, old, new, pointer); + pointer = shift_diff_ops_down(ops, old, new, pointer); + } + pointer += 1; + } + + // Then attempt to compact all Insertions + let mut pointer = 0; + while let Some(&op) = ops.get(pointer) { + if let DiffTag::Insert = op.tag() { + pointer = shift_diff_ops_up(ops, old, new, pointer); + pointer = shift_diff_ops_down(ops, old, new, pointer); + } + pointer += 1; + } +} + +fn shift_diff_ops_up( + ops: &mut Vec, + old: &Old, + new: &New, + mut pointer: usize, +) -> usize +where + Old: Index + ?Sized, + New: Index + ?Sized, + New::Output: PartialEq, +{ + while let Some(&prev_op) = pointer.checked_sub(1).and_then(|idx| ops.get(idx)) { + let this_op = ops[pointer]; + match (this_op.tag(), prev_op.tag()) { + // Shift Inserts Upwards + (DiffTag::Insert, DiffTag::Equal) => { + let suffix_len = + common_suffix_len(old, prev_op.old_range(), new, this_op.new_range()); + if suffix_len > 0 { + if let Some(DiffTag::Equal) = ops.get(pointer + 1).map(|x| x.tag()) { + ops[pointer + 1].grow_left(suffix_len); + } else { + ops.insert( + pointer + 1, + DiffOp::Equal { + old_index: prev_op.old_range().end - suffix_len, + new_index: this_op.new_range().end - suffix_len, + len: suffix_len, + }, + ); + } + ops[pointer].shift_left(suffix_len); + ops[pointer - 1].shrink_left(suffix_len); + + if ops[pointer - 1].is_empty() { + ops.remove(pointer - 1); + pointer -= 1; + } + } else if ops[pointer - 1].is_empty() { + ops.remove(pointer - 1); + pointer -= 1; + } else { + // We can't shift upwards anymore + break; + } + } + // Shift Deletions Upwards + (DiffTag::Delete, DiffTag::Equal) => { + // check common suffix for the amount we can shift + let suffix_len = + common_suffix_len(old, prev_op.old_range(), new, this_op.new_range()); + if suffix_len != 0 { + if let Some(DiffTag::Equal) = ops.get(pointer + 1).map(|x| x.tag()) { + ops[pointer + 1].grow_left(suffix_len); + } else { + let old_range = prev_op.old_range(); + ops.insert( + pointer + 1, + DiffOp::Equal { + old_index: old_range.end - suffix_len, + new_index: this_op.new_range().end - suffix_len, + len: old_range.len() - suffix_len, + }, + ); + } + ops[pointer].shift_left(suffix_len); + ops[pointer - 1].shrink_left(suffix_len); + + if ops[pointer - 1].is_empty() { + ops.remove(pointer - 1); + pointer -= 1; + } + } else if ops[pointer - 1].is_empty() { + ops.remove(pointer - 1); + pointer -= 1; + } else { + // We can't shift upwards anymore + break; + } + } + // Swap the Delete and Insert + (DiffTag::Insert, DiffTag::Delete) | (DiffTag::Delete, DiffTag::Insert) => { + ops.swap(pointer - 1, pointer); + pointer -= 1; + } + // Merge the two ranges + (DiffTag::Insert, DiffTag::Insert) => { + ops[pointer - 1].grow_right(this_op.new_range().len()); + ops.remove(pointer); + pointer -= 1; + } + (DiffTag::Delete, DiffTag::Delete) => { + ops[pointer - 1].grow_right(this_op.old_range().len()); + ops.remove(pointer); + pointer -= 1; + } + _ => unreachable!("unexpected tag"), + } + } + pointer +} + +fn shift_diff_ops_down( + ops: &mut Vec, + old: &Old, + new: &New, + mut pointer: usize, +) -> usize +where + Old: Index + ?Sized, + New: Index + ?Sized, + New::Output: PartialEq, +{ + while let Some(&next_op) = pointer.checked_add(1).and_then(|idx| ops.get(idx)) { + let this_op = ops[pointer]; + match (this_op.tag(), next_op.tag()) { + // Shift Inserts Downwards + (DiffTag::Insert, DiffTag::Equal) => { + let prefix_len = + common_prefix_len(old, next_op.old_range(), new, this_op.new_range()); + if prefix_len > 0 { + if let Some(DiffTag::Equal) = pointer + .checked_sub(1) + .and_then(|x| ops.get(x)) + .map(|x| x.tag()) + { + ops[pointer - 1].grow_right(prefix_len); + } else { + ops.insert( + pointer, + DiffOp::Equal { + old_index: next_op.old_range().start, + new_index: this_op.new_range().start, + len: prefix_len, + }, + ); + pointer += 1; + } + ops[pointer].shift_right(prefix_len); + ops[pointer + 1].shrink_right(prefix_len); + + if ops[pointer + 1].is_empty() { + ops.remove(pointer + 1); + } + } else if ops[pointer + 1].is_empty() { + ops.remove(pointer + 1); + } else { + // We can't shift upwards anymore + break; + } + } + // Shift Deletions Downwards + (DiffTag::Delete, DiffTag::Equal) => { + // check common suffix for the amount we can shift + let prefix_len = + common_prefix_len(old, next_op.old_range(), new, this_op.new_range()); + if prefix_len > 0 { + if let Some(DiffTag::Equal) = pointer + .checked_sub(1) + .and_then(|x| ops.get(x)) + .map(|x| x.tag()) + { + ops[pointer - 1].grow_right(prefix_len); + } else { + ops.insert( + pointer, + DiffOp::Equal { + old_index: next_op.old_range().start, + new_index: this_op.new_range().start, + len: prefix_len, + }, + ); + pointer += 1; + } + ops[pointer].shift_right(prefix_len); + ops[pointer + 1].shrink_right(prefix_len); + + if ops[pointer + 1].is_empty() { + ops.remove(pointer + 1); + } + } else if ops[pointer + 1].is_empty() { + ops.remove(pointer + 1); + } else { + // We can't shift downwards anymore + break; + } + } + // Swap the Delete and Insert + (DiffTag::Insert, DiffTag::Delete) | (DiffTag::Delete, DiffTag::Insert) => { + ops.swap(pointer, pointer + 1); + pointer += 1; + } + // Merge the two ranges + (DiffTag::Insert, DiffTag::Insert) => { + ops[pointer].grow_right(next_op.new_range().len()); + ops.remove(pointer + 1); + } + (DiffTag::Delete, DiffTag::Delete) => { + ops[pointer].grow_right(next_op.old_range().len()); + ops.remove(pointer + 1); + } + _ => unreachable!("unexpected tag"), + } + } + pointer +} diff --git a/src/algorithms/mod.rs b/src/algorithms/mod.rs index 23cf81c..40127b2 100644 --- a/src/algorithms/mod.rs +++ b/src/algorithms/mod.rs @@ -34,17 +34,20 @@ //! [`capture_diff_slices`](crate::capture_diff_slices). mod capture; +mod compact; mod hook; mod replace; -mod utils; +pub(crate) mod utils; use std::hash::Hash; use std::ops::{Index, Range}; use std::time::Instant; pub use capture::Capture; +pub use compact::Compact; pub use hook::{DiffHook, NoFinishHook}; pub use replace::Replace; +pub use utils::IdentifyDistinct; #[doc(no_inline)] pub use crate::Algorithm; diff --git a/src/algorithms/myers.rs b/src/algorithms/myers.rs index 08176b1..1f097d3 100644 --- a/src/algorithms/myers.rs +++ b/src/algorithms/myers.rs @@ -65,8 +65,8 @@ where New::Output: PartialEq, { let max_d = max_d(old_range.len(), new_range.len()); - let mut vf = V::new(max_d); let mut vb = V::new(max_d); + let mut vf = V::new(max_d); conquer( d, old, old_range, new, new_range, &mut vf, &mut vb, deadline, )?; diff --git a/src/algorithms/utils.rs b/src/algorithms/utils.rs index 26575fd..0d14ac7 100644 --- a/src/algorithms/utils.rs +++ b/src/algorithms/utils.rs @@ -1,8 +1,8 @@ use std::collections::hash_map::Entry; use std::collections::HashMap; use std::fmt::Debug; -use std::hash::Hash; -use std::ops::{Index, Range}; +use std::hash::{Hash, Hasher}; +use std::ops::{Add, Index, Range}; /// Utility function to check if a range is empty that works on older rust versions #[inline(always)] @@ -142,6 +142,175 @@ where .count() } +struct OffsetLookup { + offset: usize, + vec: Vec, +} + +impl Index for OffsetLookup { + type Output = Int; + + #[inline(always)] + fn index(&self, index: usize) -> &Self::Output { + &self.vec[index - self.offset] + } +} + +/// A utility struct to convert distinct items to unique integers. +/// +/// This can be helpful on larger inputs to speed up the comparisons +/// performed by doing a first pass where the data set gets reduced +/// to (small) integers. +/// +/// The idea is that instead of passing two sequences to a diffling algorithm +/// you first pass it via [`IdentifyDistinct`]: +/// +/// ```rust +/// use similar::capture_diff; +/// use similar::algorithms::{Algorithm, IdentifyDistinct}; +/// +/// let old = &["foo", "bar", "baz"][..]; +/// let new = &["foo", "blah", "baz"][..]; +/// let h = IdentifyDistinct::::new(old, 0..old.len(), new, 0..new.len()); +/// let ops = capture_diff( +/// Algorithm::Myers, +/// h.old_lookup(), +/// h.old_range(), +/// h.new_lookup(), +/// h.new_range(), +/// ); +/// ``` +/// +/// The indexes are the same as with the passed source ranges. +pub struct IdentifyDistinct { + old: OffsetLookup, + new: OffsetLookup, +} + +impl IdentifyDistinct +where + Int: Add + From + Default + Copy, +{ + /// Creates an int hasher for two sequences. + pub fn new( + old: &Old, + old_range: Range, + new: &New, + new_range: Range, + ) -> Self + where + Old: Index + ?Sized, + Old::Output: Eq + Hash, + New: Index + ?Sized, + New::Output: Eq + Hash + PartialEq, + { + enum Key<'old, 'new, Old: ?Sized, New: ?Sized> { + Old(&'old Old), + New(&'new New), + } + + impl<'old, 'new, Old, New> Hash for Key<'old, 'new, Old, New> + where + Old: Hash + ?Sized, + New: Hash + ?Sized, + { + fn hash(&self, state: &mut H) { + match *self { + Key::Old(val) => val.hash(state), + Key::New(val) => val.hash(state), + } + } + } + + impl<'old, 'new, Old, New> PartialEq for Key<'old, 'new, Old, New> + where + Old: Eq + ?Sized, + New: Eq + PartialEq + ?Sized, + { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Key::Old(a), Key::Old(b)) => a == b, + (Key::New(a), Key::New(b)) => a == b, + (Key::Old(a), Key::New(b)) | (Key::New(b), Key::Old(a)) => b == a, + } + } + } + + impl<'old, 'new, Old, New> Eq for Key<'old, 'new, Old, New> + where + Old: Eq + ?Sized, + New: Eq + PartialEq + ?Sized, + { + } + + let mut map = HashMap::new(); + let mut old_seq = Vec::new(); + let mut new_seq = Vec::new(); + let mut next_id = Int::default(); + let step = Int::from(1); + let old_start = old_range.start; + let new_start = new_range.start; + + for idx in old_range { + let item = Key::Old(&old[idx]); + let id = match map.entry(item) { + Entry::Occupied(o) => *o.get(), + Entry::Vacant(v) => { + let id = next_id; + next_id = next_id + step; + *v.insert(id) + } + }; + old_seq.push(id); + } + + for idx in new_range { + let item = Key::New(&new[idx]); + let id = match map.entry(item) { + Entry::Occupied(o) => *o.get(), + Entry::Vacant(v) => { + let id = next_id; + next_id = next_id + step; + *v.insert(id) + } + }; + new_seq.push(id); + } + + IdentifyDistinct { + old: OffsetLookup { + offset: old_start, + vec: old_seq, + }, + new: OffsetLookup { + offset: new_start, + vec: new_seq, + }, + } + } + + /// Returns a lookup for the old side. + pub fn old_lookup(&self) -> &impl Index { + &self.old + } + + /// Returns a lookup for the new side. + pub fn new_lookup(&self) -> &impl Index { + &self.new + } + + /// Convenience method to get back the old range. + pub fn old_range(&self) -> Range { + self.old.offset..self.old.offset + self.old.vec.len() + } + + /// Convenience method to get back the new range. + pub fn new_range(&self) -> Range { + self.new.offset..self.new.offset + self.new.vec.len() + } +} + #[test] fn test_unique() { let u = unique(&vec!['a', 'b', 'c', 'd', 'd', 'b'], 0..6) @@ -151,6 +320,24 @@ fn test_unique() { assert_eq!(u, vec![('a', 0), ('c', 2)]); } +#[test] +fn test_int_hasher() { + let ih = IdentifyDistinct::::new( + &["", "foo", "bar", "baz"][..], + 1..4, + &["", "foo", "blah", "baz"][..], + 1..4, + ); + assert_eq!(ih.old_lookup()[1], 0); + assert_eq!(ih.old_lookup()[2], 1); + assert_eq!(ih.old_lookup()[3], 2); + assert_eq!(ih.new_lookup()[1], 0); + assert_eq!(ih.new_lookup()[2], 3); + assert_eq!(ih.new_lookup()[3], 2); + assert_eq!(ih.old_range(), 1..4); + assert_eq!(ih.new_range(), 1..4); +} + #[test] fn test_common_prefix_len() { assert_eq!( diff --git a/src/common.rs b/src/common.rs index 6e4d6a4..35e1a16 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2,14 +2,14 @@ use std::hash::Hash; use std::ops::{Index, Range}; use std::time::Instant; -use crate::algorithms::{diff_deadline, diff_slices_deadline, Capture, Replace}; +use crate::algorithms::{diff_deadline, Capture, Compact, Replace}; use crate::{Algorithm, DiffOp}; /// Creates a diff between old and new with the given algorithm capturing the ops. /// /// This is like [`diff`](crate::algorithms::diff) but instead of using an -/// arbitrary hook this will always use [`Replace`] + [`Capture`] and return the -/// captured [`DiffOp`]s. +/// arbitrary hook this will always use [`Compact`] + [`Replace`] + [`Capture`] +/// and return the captured [`DiffOp`]s. pub fn capture_diff( alg: Algorithm, old: &Old, @@ -43,9 +43,9 @@ where Old::Output: Hash + Eq + Ord, New::Output: PartialEq + Hash + Eq + Ord, { - let mut d = Replace::new(Capture::new()); + let mut d = Compact::new(Replace::new(Capture::new()), old, new); diff_deadline(alg, &mut d, old, old_range, new, new_range, deadline).unwrap(); - d.into_inner().into_ops() + d.into_inner().into_inner().into_ops() } /// Creates a diff between old and new with the given algorithm capturing the ops. @@ -68,9 +68,7 @@ pub fn capture_diff_slices_deadline( where T: Eq + Hash + Ord, { - let mut d = Replace::new(Capture::new()); - diff_slices_deadline(alg, &mut d, old, new, deadline).unwrap(); - d.into_inner().into_ops() + capture_diff_deadline(alg, old, 0..old.len(), new, 0..new.len(), deadline) } /// Return a measure of similarity in the range `0..=1`. diff --git a/src/text/mod.rs b/src/text/mod.rs index dd9cf13..1d647fe 100644 --- a/src/text/mod.rs +++ b/src/text/mod.rs @@ -14,9 +14,10 @@ pub use self::abstraction::{DiffableStr, DiffableStrRef}; pub use self::inline::InlineChange; use self::utils::{upper_seq_ratio, QuickSeqRatio}; +use crate::algorithms::IdentifyDistinct; use crate::iter::{AllChangesIter, ChangesIter}; use crate::udiff::UnifiedDiff; -use crate::{capture_diff_slices_deadline, get_diff_ratio, group_diff_ops, Algorithm, DiffOp}; +use crate::{capture_diff_deadline, get_diff_ratio, group_diff_ops, Algorithm, DiffOp}; #[derive(Debug, Clone, Copy)] enum Deadline { @@ -327,12 +328,27 @@ impl TextDiffConfig { new: Cow<'bufs, [&'new T]>, newline_terminated: bool, ) -> TextDiff<'old, 'new, 'bufs, T> { - let ops = capture_diff_slices_deadline( - self.algorithm, - &old, - &new, - self.deadline.map(|x| x.into_instant()), - ); + let deadline = self.deadline.map(|x| x.into_instant()); + let ops = if old.len() > 100 || new.len() > 100 { + let ih = IdentifyDistinct::::new(&old[..], 0..old.len(), &new[..], 0..new.len()); + capture_diff_deadline( + self.algorithm, + ih.old_lookup(), + ih.old_range(), + ih.new_lookup(), + ih.new_range(), + deadline, + ) + } else { + capture_diff_deadline( + self.algorithm, + &old[..], + 0..old.len(), + &new[..], + 0..new.len(), + deadline, + ) + }; TextDiff { old, new, diff --git a/src/types.rs b/src/types.rs index 8654550..4a1cead 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,6 +1,7 @@ use std::fmt; use std::ops::{Index, Range}; +use crate::algorithms::utils::is_empty_range; use crate::algorithms::DiffHook; use crate::iter::ChangesIter; @@ -343,6 +344,87 @@ impl DiffOp { .chain(Some((ChangeTag::Insert, &new[new_index..new_index + new_len])).into_iter()), } } + + pub(crate) fn is_empty(&self) -> bool { + let (_, old, new) = self.as_tag_tuple(); + is_empty_range(&old) && is_empty_range(&new) + } + + pub(crate) fn shift_left(&mut self, adjust: usize) { + self.adjust((adjust, true), (0, false)); + } + + pub(crate) fn shift_right(&mut self, adjust: usize) { + self.adjust((adjust, false), (0, false)); + } + + pub(crate) fn grow_left(&mut self, adjust: usize) { + self.adjust((adjust, true), (adjust, false)); + } + + pub(crate) fn grow_right(&mut self, adjust: usize) { + self.adjust((0, false), (adjust, false)); + } + + pub(crate) fn shrink_left(&mut self, adjust: usize) { + self.adjust((0, false), (adjust, true)); + } + + pub(crate) fn shrink_right(&mut self, adjust: usize) { + self.adjust((adjust, false), (adjust, true)); + } + + fn adjust(&mut self, adjust_offset: (usize, bool), adjust_len: (usize, bool)) { + #[inline(always)] + fn modify(val: &mut usize, adj: (usize, bool)) { + if adj.1 { + *val -= adj.0; + } else { + *val += adj.0; + } + } + + match self { + DiffOp::Equal { + old_index, + new_index, + len, + } => { + modify(old_index, adjust_offset); + modify(new_index, adjust_offset); + modify(len, adjust_len); + } + DiffOp::Delete { + old_index, + old_len, + new_index, + } => { + modify(old_index, adjust_offset); + modify(old_len, adjust_len); + modify(new_index, adjust_offset); + } + DiffOp::Insert { + old_index, + new_index, + new_len, + } => { + modify(old_index, adjust_offset); + modify(new_index, adjust_offset); + modify(new_len, adjust_len); + } + DiffOp::Replace { + old_index, + old_len, + new_index, + new_len, + } => { + modify(old_index, adjust_offset); + modify(old_len, adjust_len); + modify(new_index, adjust_offset); + modify(new_len, adjust_len); + } + } + } } #[cfg(feature = "text")]