//! 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 }