use alloc::vec::Vec;
use core::hash::Hash;
use core::ops::{Index, Range};
use crate::{
Algorithm, ChangeTag, DiffOp, DiffableStr, DiffableStrRef, TextDiff, capture_diff_slices,
};
#[cfg(feature = "inline")]
use crate::{InlineChange, InlineChangeOptions};
struct SliceRemapper<'x, T: ?Sized> {
source: &'x T,
indexes: Vec<Range<usize>>,
}
impl<'x, T: DiffableStr + ?Sized> SliceRemapper<'x, T> {
fn from_lengths(
source: &'x T,
lengths: impl IntoIterator<Item = usize>,
) -> SliceRemapper<'x, T> {
let mut offset = 0;
let mut indexes = Vec::new();
for len in lengths {
let end = offset + len;
indexes.push(offset..end);
offset = end;
}
SliceRemapper { source, indexes }
}
fn new(source: &'x T, slices: &[&'x T]) -> SliceRemapper<'x, T> {
Self::from_lengths(source, slices.iter().map(|item| item.len()))
}
fn slice(&self, range: Range<usize>) -> Option<&'x T> {
let start = self.indexes.get(range.start)?.start;
let end = self.indexes.get(range.end - 1)?.end;
Some(self.source.slice(start..end))
}
}
impl<T: DiffableStr + ?Sized> Index<Range<usize>> for SliceRemapper<'_, T> {
type Output = T;
fn index(&self, range: Range<usize>) -> &Self::Output {
self.slice(range).expect("out of bounds")
}
}
pub struct TextDiffRemapper<'x, T: ?Sized> {
old: SliceRemapper<'x, T>,
new: SliceRemapper<'x, T>,
}
impl<'x, T: DiffableStr + ?Sized> TextDiffRemapper<'x, T> {
pub fn new(
old_slices: &[&'x T],
new_slices: &[&'x T],
old: &'x T,
new: &'x T,
) -> TextDiffRemapper<'x, T> {
TextDiffRemapper {
old: SliceRemapper::new(old, old_slices),
new: SliceRemapper::new(new, new_slices),
}
}
pub fn from_text_diff<'old, 'new>(
diff: &TextDiff<'old, 'new, T>,
old: &'x T,
new: &'x T,
) -> TextDiffRemapper<'x, T>
where
'old: 'x,
'new: 'x,
{
TextDiffRemapper {
old: SliceRemapper::from_lengths(
old,
(0..diff.old_len())
.map(|idx| diff.old_slice(idx).expect("slice out of bounds").len()),
),
new: SliceRemapper::from_lengths(
new,
(0..diff.new_len())
.map(|idx| diff.new_slice(idx).expect("slice out of bounds").len()),
),
}
}
pub fn slice_old(&self, range: Range<usize>) -> Option<&'x T> {
self.old.slice(range)
}
pub fn slice_new(&self, range: Range<usize>) -> Option<&'x T> {
self.new.slice(range)
}
pub fn iter_slices(&self, op: &DiffOp) -> impl Iterator<Item = (ChangeTag, &'x T)> {
match *op {
DiffOp::Equal { old_index, len, .. } => {
Some((ChangeTag::Equal, self.old.slice(old_index..old_index + len)))
.into_iter()
.chain(None)
}
DiffOp::Insert {
new_index, new_len, ..
} => Some((
ChangeTag::Insert,
self.new.slice(new_index..new_index + new_len),
))
.into_iter()
.chain(None),
DiffOp::Delete {
old_index, old_len, ..
} => Some((
ChangeTag::Delete,
self.old.slice(old_index..old_index + old_len),
))
.into_iter()
.chain(None),
DiffOp::Replace {
old_index,
old_len,
new_index,
new_len,
} => Some((
ChangeTag::Delete,
self.old.slice(old_index..old_index + old_len),
))
.into_iter()
.chain(Some((
ChangeTag::Insert,
self.new.slice(new_index..new_index + new_len),
))),
}
.map(|(tag, opt_val)| (tag, opt_val.expect("slice out of bounds")))
}
}
pub fn diff_slices<'x, T: PartialEq + Hash + Ord>(
alg: Algorithm,
old: &'x [T],
new: &'x [T],
) -> Vec<(ChangeTag, &'x [T])> {
capture_diff_slices(alg, old, new)
.iter()
.flat_map(|op| op.iter_slices(old, new))
.collect()
}
pub fn diff_chars<'x, T: DiffableStrRef + ?Sized>(
alg: Algorithm,
old: &'x T,
new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
let old = old.as_diffable_str();
let new = new.as_diffable_str();
let diff = TextDiff::configure().algorithm(alg).diff_chars(old, new);
let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
let mut rv = Vec::new();
for op in diff.ops() {
rv.extend(remapper.iter_slices(op));
}
rv
}
pub fn diff_words<'x, T: DiffableStrRef + ?Sized>(
alg: Algorithm,
old: &'x T,
new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
let old = old.as_diffable_str();
let new = new.as_diffable_str();
let diff = TextDiff::configure().algorithm(alg).diff_words(old, new);
let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
let mut rv = Vec::new();
for op in diff.ops() {
rv.extend(remapper.iter_slices(op));
}
rv
}
#[cfg(feature = "unicode")]
pub fn diff_unicode_words<'x, T: DiffableStrRef + ?Sized>(
alg: Algorithm,
old: &'x T,
new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
let old = old.as_diffable_str();
let new = new.as_diffable_str();
let diff = TextDiff::configure()
.algorithm(alg)
.diff_unicode_words(old, new);
let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
let mut rv = Vec::new();
for op in diff.ops() {
rv.extend(remapper.iter_slices(op));
}
rv
}
#[cfg(feature = "unicode")]
pub fn diff_graphemes<'x, T: DiffableStrRef + ?Sized>(
alg: Algorithm,
old: &'x T,
new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
let old = old.as_diffable_str();
let new = new.as_diffable_str();
let diff = TextDiff::configure()
.algorithm(alg)
.diff_graphemes(old, new);
let remapper = TextDiffRemapper::from_text_diff(&diff, old, new);
let mut rv = Vec::new();
for op in diff.ops() {
rv.extend(remapper.iter_slices(op));
}
rv
}
pub fn diff_lines<'x, T: DiffableStrRef + ?Sized>(
alg: Algorithm,
old: &'x T,
new: &'x T,
) -> Vec<(ChangeTag, &'x T::Output)> {
let old = old.as_diffable_str();
let new = new.as_diffable_str();
let old_slices = old.tokenize_lines();
let new_slices = new.tokenize_lines();
capture_diff_slices(alg, &old_slices, &new_slices)
.iter()
.flat_map(|op| op.iter_changes(&old_slices, &new_slices))
.map(|change| (change.tag(), change.value()))
.collect()
}
#[cfg(feature = "inline")]
pub fn diff_lines_inline<'diff, 'old, 'new, T: DiffableStr + ?Sized>(
diff: &'diff TextDiff<'old, 'new, T>,
options: InlineChangeOptions,
) -> Vec<InlineChange<'diff, T>> {
diff.iter_all_inline_changes_with_options(options).collect()
}
#[test]
fn test_remapper() {
let a = "foo bar baz";
let words = a.tokenize_words();
dbg!(&words);
let remap = SliceRemapper::new(a, &words);
assert_eq!(remap.slice(0..3), Some("foo bar"));
assert_eq!(remap.slice(1..3), Some(" bar"));
assert_eq!(remap.slice(0..1), Some("foo"));
assert_eq!(remap.slice(0..5), Some("foo bar baz"));
assert_eq!(remap.slice(0..6), None);
}