use crate::engine;
use crate::types::{Diff, DiffToken, Dmp, TDiff};
use std::time::{Duration, Instant};
#[allow(clippy::ptr_arg)]
impl Dmp {
pub fn diff_main(&mut self, text1: &str, text2: &str, checklines: bool) -> Vec<Diff> {
let deadline = self.deadline_from_now();
#[cfg(feature = "grapheme")]
{
if self.segmentation == crate::types::Segmentation::Grapheme {
let mut packer = crate::tokenize::GraphemePacker::new(&[text1, text2]);
let packed1 = packer.pack(text1);
let packed2 = packer.pack(text2);
let mut scratch = Vec::new();
let mut diffs = main_internal(
self,
&packed1,
&packed2,
checklines,
true,
deadline,
&mut scratch,
);
packer.unpack_diffs(&mut diffs);
return diffs;
}
}
main_internal(
self,
text1,
text2,
checklines,
true,
deadline,
&mut Vec::new(),
)
}
fn deadline_from_now(&self) -> Option<Instant> {
let secs = self.diff_timeout?;
if secs.is_nan() {
return None;
}
Some(Instant::now() + Duration::from_secs_f32(secs.clamp(0.0, 1.0e9)))
}
pub fn diff_linemode(&mut self, text1: &Vec<char>, text2: &Vec<char>) -> Vec<Diff> {
#[cfg(feature = "grapheme")]
{
if self.segmentation == crate::types::Segmentation::Grapheme {
let t1: String = text1.iter().collect();
let t2: String = text2.iter().collect();
return self.diff_main(&t1, &t2, true);
}
}
let deadline = self.deadline_from_now();
materialize(line_mode(self, text1, text2, deadline, &mut Vec::new()))
}
pub fn diff_bisect(&mut self, char1: &Vec<char>, char2: &Vec<char>) -> Vec<Diff> {
let deadline = self.deadline_from_now();
materialize(bisect_diff(
self,
char1,
char2,
true,
deadline,
&mut Vec::new(),
))
}
pub fn diff_common_prefix(&mut self, text1: &Vec<char>, text2: &Vec<char>) -> i32 {
engine::common_prefix(text1, text2) as i32
}
pub fn diff_common_suffix(&mut self, text1: &Vec<char>, text2: &Vec<char>) -> i32 {
engine::common_suffix(text1, text2) as i32
}
pub fn diff_common_overlap(&mut self, text1: &Vec<char>, text2: &Vec<char>) -> i32 {
engine::common_overlap(text1, text2) as i32
}
pub fn diff_half_match(&mut self, text1: &Vec<char>, text2: &Vec<char>) -> Vec<String> {
if self.deadline_from_now().is_none() {
return vec![];
}
match engine::half_match(text1, text2) {
None => vec![],
Some(hm) => {
let (text1_a, text1_b, text2_a, text2_b, mid_common) =
split_half_match(text1, text2, &hm);
vec![text1_a, text1_b, text2_a, text2_b, mid_common]
}
}
}
}
fn split_half_match(
old: &[char],
new: &[char],
hm: &engine::HalfMatch,
) -> (String, String, String, String, String) {
(
old[..hm.old_a].iter().collect(),
old[hm.old_a + hm.common..].iter().collect(),
new[..hm.new_a].iter().collect(),
new[hm.new_a + hm.common..].iter().collect(),
old[hm.old_a..hm.old_a + hm.common].iter().collect(),
)
}
fn materialize(tokens: Vec<TDiff>) -> Vec<Diff> {
tokens.into_iter().map(TDiff::into_diff).collect()
}
fn main_internal(
dmp: &mut Dmp,
text1: &str,
text2: &str,
checklines: bool,
allow_words: bool,
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<Diff> {
if text1.is_empty() && text2.is_empty() {
return vec![];
} else if text1.is_empty() {
return vec![Diff::new(1, text2.to_string())];
} else if text2.is_empty() {
return vec![Diff::new(-1, text1.to_string())];
}
if text1 == text2 {
return vec![Diff::new(0, text1.to_string())];
}
materialize(diff_str_tokens(
dmp,
text1,
text2,
checklines,
allow_words,
deadline,
scratch,
))
}
fn diff_str_tokens(
dmp: &mut Dmp,
text1: &str,
text2: &str,
checklines: bool,
allow_words: bool,
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<TDiff> {
if text1.is_empty() && text2.is_empty() {
return vec![];
} else if text1.is_empty() {
return vec![TDiff::new(1, text2.chars().collect())];
} else if text2.is_empty() {
return vec![TDiff::new(-1, text1.chars().collect())];
}
if text1 == text2 {
return vec![TDiff::new(0, text1.chars().collect())];
}
if text1.is_ascii() && text2.is_ascii() {
return main_slices(
dmp,
text1.as_bytes(),
text2.as_bytes(),
checklines,
allow_words,
deadline,
scratch,
);
}
let char1: Vec<char> = text1.chars().collect();
let char2: Vec<char> = text2.chars().collect();
main_slices(
dmp,
&char1,
&char2,
checklines,
allow_words,
deadline,
scratch,
)
}
fn diff_char_tokens(
dmp: &mut Dmp,
old: &[char],
new: &[char],
checklines: bool,
allow_words: bool,
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<TDiff> {
if old.is_empty() && new.is_empty() {
return vec![];
} else if old.is_empty() {
return vec![TDiff::new(1, new.to_vec())];
} else if new.is_empty() {
return vec![TDiff::new(-1, old.to_vec())];
}
if old == new {
return vec![TDiff::new(0, old.to_vec())];
}
if old.iter().all(char::is_ascii) && new.iter().all(char::is_ascii) {
let b1: Vec<u8> = old.iter().map(|&c| c as u8).collect();
let b2: Vec<u8> = new.iter().map(|&c| c as u8).collect();
return main_slices(dmp, &b1, &b2, checklines, allow_words, deadline, scratch);
}
main_slices(dmp, old, new, checklines, allow_words, deadline, scratch)
}
pub(crate) fn diff_main_chars(
dmp: &mut Dmp,
old: &[char],
new: &[char],
checklines: bool,
) -> Vec<Diff> {
#[cfg(feature = "grapheme")]
{
if dmp.segmentation == crate::types::Segmentation::Grapheme {
let t1: String = old.iter().collect();
let t2: String = new.iter().collect();
return dmp.diff_main(&t1, &t2, checklines);
}
}
let deadline = dmp.deadline_from_now();
materialize(diff_char_tokens(
dmp,
old,
new,
checklines,
true,
deadline,
&mut Vec::new(),
))
}
fn main_slices<T: DiffToken>(
dmp: &mut Dmp,
old: &[T],
new: &[T],
checklines: bool,
allow_words: bool,
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<TDiff> {
if old.is_empty() && new.is_empty() {
return vec![];
} else if old.is_empty() {
return vec![TDiff::new(1, T::to_tokens(new))];
} else if new.is_empty() {
return vec![TDiff::new(-1, T::to_tokens(old))];
}
if old == new {
return vec![TDiff::new(0, T::to_tokens(old))];
}
let prefix = engine::common_prefix(old, new);
let suffix = engine::common_suffix(&old[prefix..], &new[prefix..]);
let mid1 = &old[prefix..old.len() - suffix];
let mid2 = &new[prefix..new.len() - suffix];
let mut diffs: Vec<TDiff> = Vec::new();
if prefix > 0 {
diffs.push(TDiff::new(0, T::to_tokens(&old[..prefix])));
}
diffs.extend(compute(
dmp,
mid1,
mid2,
checklines,
allow_words,
deadline,
scratch,
));
if suffix > 0 {
diffs.push(TDiff::new(0, T::to_tokens(&old[old.len() - suffix..])));
}
dmp.diff_cleanup_merge_impl(&mut diffs);
diffs
}
fn compute<T: DiffToken>(
dmp: &mut Dmp,
old: &[T],
new: &[T],
checklines: bool,
allow_words: bool,
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<TDiff> {
if old.is_empty() {
return vec![TDiff::new(1, T::to_tokens(new))];
}
if new.is_empty() {
return vec![TDiff::new(-1, T::to_tokens(old))];
}
{
let (long, short) = if old.len() >= new.len() {
(old, new)
} else {
(new, old)
};
if let Some(i) = engine::contains(long, short) {
let op = if old.len() > new.len() { -1 } else { 1 };
let mut diffs: Vec<TDiff> = Vec::new();
if i != 0 {
diffs.push(TDiff::new(op, T::to_tokens(&long[..i])));
}
diffs.push(TDiff::new(0, T::to_tokens(short)));
if i + short.len() != long.len() {
diffs.push(TDiff::new(op, T::to_tokens(&long[i + short.len()..])));
}
return diffs;
}
if short.len() == 1 {
return vec![
TDiff::new(-1, T::to_tokens(old)),
TDiff::new(1, T::to_tokens(new)),
];
}
}
if deadline.is_some() {
if let Some(hm) = engine::half_match(old, new) {
let mid_common: Vec<char> = T::to_tokens(&old[hm.old_a..hm.old_a + hm.common]);
let mut diffs = main_slices(
dmp,
&old[..hm.old_a],
&new[..hm.new_a],
checklines,
allow_words,
deadline,
scratch,
);
diffs.push(TDiff::new(0, mid_common));
diffs.extend(main_slices(
dmp,
&old[hm.old_a + hm.common..],
&new[hm.new_a + hm.common..],
checklines,
allow_words,
deadline,
scratch,
));
return diffs;
}
}
if checklines && old.len() > 100 && new.len() > 100 {
return line_mode(dmp, old, new, deadline, scratch);
}
if allow_words && dmp.word_mode && old.len() > 100 && new.len() > 100 {
return word_mode(dmp, old, new, deadline, scratch);
}
bisect_diff(dmp, old, new, allow_words, deadline, scratch)
}
fn bisect_diff<T: DiffToken>(
dmp: &mut Dmp,
old: &[T],
new: &[T],
allow_words: bool,
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<TDiff> {
match engine::bisect(old, new, deadline, scratch) {
Some((x, y)) => {
let mut diffs = main_slices(
dmp,
&old[..x],
&new[..y],
false,
allow_words,
deadline,
scratch,
);
diffs.extend(main_slices(
dmp,
&old[x..],
&new[y..],
false,
allow_words,
deadline,
scratch,
));
diffs
}
None => {
vec![
TDiff::new(-1, T::to_tokens(old)),
TDiff::new(1, T::to_tokens(new)),
]
}
}
}
fn line_mode<T: DiffToken>(
dmp: &mut Dmp,
old: &[T],
new: &[T],
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<TDiff> {
let mut diffs: Vec<TDiff> =
if crate::tokenize::packs_to_one_line(old) && crate::tokenize::packs_to_one_line(new) {
vec![
TDiff::new(-1, T::to_tokens(old)),
TDiff::new(1, T::to_tokens(new)),
]
} else {
let (text3, text4, store) = crate::tokenize::lines_tochars_arena(old, new);
let mut diffs: Vec<TDiff> =
diff_str_tokens(dmp, &text3, &text4, false, false, deadline, scratch);
crate::tokenize::chars_tolines_arena(&mut diffs, &store);
diffs
};
dmp.diff_cleanup_semantic_impl(&mut diffs);
rediff_blocks(dmp, diffs, true, deadline, scratch)
}
fn word_mode<T: DiffToken>(
dmp: &mut Dmp,
old: &[T],
new: &[T],
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<TDiff> {
let (text3, text4, store) = crate::tokenize::words_tochars_arena(old, new);
let mut diffs: Vec<TDiff> =
diff_str_tokens(dmp, &text3, &text4, false, false, deadline, scratch);
crate::tokenize::chars_tolines_arena(&mut diffs, &store);
rediff_blocks(dmp, diffs, false, deadline, scratch)
}
fn rediff_blocks(
dmp: &mut Dmp,
mut diffs: Vec<TDiff>,
allow_words: bool,
deadline: Option<Instant>,
scratch: &mut Vec<i32>,
) -> Vec<TDiff> {
diffs.push(TDiff::new(0, vec![]));
let mut count_delete = 0;
let mut count_insert = 0;
let mut text_delete: Vec<char> = vec![];
let mut text_insert: Vec<char> = vec![];
let mut pointer = 0;
let mut temp: Vec<TDiff> = vec![];
while pointer < diffs.len() {
if diffs[pointer].operation == 1 {
count_insert += 1;
text_insert.extend_from_slice(&diffs[pointer].data);
} else if diffs[pointer].operation == -1 {
count_delete += 1;
text_delete.extend_from_slice(&diffs[pointer].data);
} else {
if count_delete >= 1 && count_insert >= 1 {
let sub_diff = diff_char_tokens(
dmp,
&text_delete,
&text_insert,
false,
allow_words,
deadline,
scratch,
);
for z in sub_diff {
temp.push(z);
}
temp.push(TDiff::new(
diffs[pointer].operation,
diffs[pointer].data.clone(),
));
} else {
if !text_delete.is_empty() {
temp.push(TDiff::new(-1, std::mem::take(&mut text_delete)));
}
if !text_insert.is_empty() {
temp.push(TDiff::new(1, std::mem::take(&mut text_insert)));
}
temp.push(TDiff::new(
diffs[pointer].operation,
diffs[pointer].data.clone(),
));
}
count_delete = 0;
count_insert = 0;
text_delete = vec![];
text_insert = vec![];
}
pointer += 1;
}
temp.pop(); temp
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_byte_path_matches_char_path() {
let long_multiline = (0..60)
.map(|i| format!("row {i}: alpha beta gamma delta\n"))
.collect::<String>();
let long_multiline_edited = long_multiline.replace("beta", "BETA");
let long_single = "words and more words ".repeat(12);
let long_single_edited = format!("{}TAIL", &long_single[..long_single.len() - 6]);
let cases: &[(&str, &str)] = &[
(
"The quick brown fox jumps over the lazy dog.",
"The quick red fox leaps over the sleepy dog!",
),
("abcdefghij", "abcxyzghij"),
(&long_multiline, &long_multiline_edited),
(&long_single, &long_single_edited),
];
for word_mode in [false, true] {
for (t1, t2) in cases {
let mut dmp = Dmp::new();
dmp.word_mode = word_mode;
let byte_diff = main_slices(
&mut dmp,
t1.as_bytes(),
t2.as_bytes(),
true,
true,
None,
&mut Vec::new(),
);
let c1: Vec<char> = t1.chars().collect();
let c2: Vec<char> = t2.chars().collect();
let char_diff = main_slices(&mut dmp, &c1, &c2, true, true, None, &mut Vec::new());
assert_eq!(byte_diff.len(), char_diff.len(), "{t1:?} vs {t2:?}");
for (b, c) in byte_diff.iter().zip(char_diff.iter()) {
assert_eq!(b.operation, c.operation, "{t1:?} vs {t2:?}");
assert_eq!(b.data, c.data, "{t1:?} vs {t2:?}");
}
}
}
}
}