use super::{Change, DiffLine};
const MAX_CELLS: usize = 4_000_000;
const NO_NEWLINE: &str = r"\ No newline at end of file";
const CARRIAGE_RETURN: char = '\u{240d}';
pub(super) fn diff(before: &str, after: &str, context: usize) -> Vec<DiffLine> {
let old: Vec<&str> = before.lines().collect();
let new: Vec<&str> = after.lines().collect();
if old == new {
if before == after {
return Vec::new();
}
return endings(before, after, context);
}
let prefix = old.iter().zip(&new).take_while(|(a, b)| a == b).count();
let room = old.len().min(new.len()) - prefix;
let suffix = old.iter().rev().zip(new.iter().rev()).take(room).take_while(|(a, b)| a == b).count();
let mut out = Vec::new();
push(&mut out, Change::Same, &old[prefix.saturating_sub(context)..prefix]);
middle(&old[prefix..old.len() - suffix], &new[prefix..new.len() - suffix], &mut out);
let tail = old.len() - suffix;
push(&mut out, Change::Same, &old[tail..tail + suffix.min(context)]);
out
}
fn push(out: &mut Vec<DiffLine>, change: Change, lines: &[&str]) {
out.extend(lines.iter().map(|text| DiffLine { change, text: (*text).to_owned() }));
}
fn endings(before: &str, after: &str, context: usize) -> Vec<DiffLine> {
let old: Vec<&str> = before.split_inclusive('\n').collect();
let new: Vec<&str> = after.split_inclusive('\n').collect();
let mut out = Vec::new();
let Some(first) = old.iter().zip(&new).position(|(a, b)| a != b) else {
return out;
};
let last = (first..old.len()).rev().find(|at| old[*at] != new[*at]).unwrap_or(first);
let tail = last + 1;
push_ending(&mut out, Change::Same, &old[first.saturating_sub(context)..first]);
push_ending(&mut out, Change::Removed, &old[first..=last]);
push_ending(&mut out, Change::Added, &new[first..=last]);
push_ending(&mut out, Change::Same, &old[tail..(tail + context).min(old.len())]);
out
}
fn push_ending(out: &mut Vec<DiffLine>, change: Change, lines: &[&str]) {
for line in lines {
if let Some(text) = line.strip_suffix("\r\n") {
out.push(DiffLine { change, text: format!("{text} {CARRIAGE_RETURN}") });
} else {
out.push(DiffLine { change, text: line.strip_suffix('\n').unwrap_or(line).to_owned() });
if !line.ends_with('\n') {
out.push(DiffLine { change, text: NO_NEWLINE.to_owned() });
}
}
}
}
fn middle(old: &[&str], new: &[&str], out: &mut Vec<DiffLine>) {
if old.len().saturating_mul(new.len()) > MAX_CELLS {
push(out, Change::Removed, old);
push(out, Change::Added, new);
return;
}
let width = new.len() + 1;
let mut common = vec![0_u32; (old.len() + 1) * width];
for i in (0..old.len()).rev() {
for j in (0..new.len()).rev() {
common[i * width + j] = if old[i] == new[j] {
common[(i + 1) * width + j + 1] + 1
} else {
common[(i + 1) * width + j].max(common[i * width + j + 1])
};
}
}
let (mut i, mut j) = (0, 0);
while i < old.len() && j < new.len() {
if old[i] == new[j] {
push(out, Change::Same, &old[i..=i]);
i += 1;
j += 1;
} else if common[(i + 1) * width + j] >= common[i * width + j + 1] {
push(out, Change::Removed, &old[i..=i]);
i += 1;
} else {
push(out, Change::Added, &new[j..=j]);
j += 1;
}
}
push(out, Change::Removed, &old[i..]);
push(out, Change::Added, &new[j..]);
}