use core::fmt::Write as _;
use camino::Utf8Path;
#[must_use]
pub fn diff(path: &Utf8Path, before: &str, after: &str) -> String {
let old: Vec<&str> = before.lines().collect();
let new: Vec<&str> = after.lines().collect();
let mut out = format!("--- {path}\n+++ {path}\n");
for step in script(&old, &new) {
let _ = match step {
Step::Kept(line) => writeln!(out, " {line}"),
Step::Added(line) => writeln!(out, "+{line}"),
Step::Removed(line) => writeln!(out, "-{line}"),
};
}
out
}
enum Step<'a> {
Kept(&'a str),
Added(&'a str),
Removed(&'a str),
}
const DIFF_LIMIT: usize = 2_000;
#[expect(
clippy::many_single_char_names,
reason = "n, m, d, k, x and y are Myers' own names for these; renaming them would make the algorithm harder to check against the paper, not easier"
)]
fn script<'a>(old: &[&'a str], new: &[&'a str]) -> Vec<Step<'a>> {
let (n, m) = (old.len(), new.len());
let max = n + m;
if max == 0 {
return Vec::new();
}
if max > DIFF_LIMIT && old != new {
let mut steps: Vec<Step<'a>> = old.iter().map(|line| Step::Removed(line)).collect();
steps.extend(new.iter().map(|line| Step::Added(line)));
return steps;
}
let mut furthest = vec![0_isize; 2 * max + 1];
let mut rounds: Vec<isize> = Vec::new();
let shift = |k: isize| usize::try_from(k + isize::try_from(max).unwrap_or(isize::MAX)).unwrap_or(0);
for d in 0..=isize::try_from(max).unwrap_or(isize::MAX) {
rounds.extend_from_slice(furthest.get(shift(-d)..=shift(d)).unwrap_or_default());
let mut k = -d;
while k <= d {
let down = k == -d || (k != d && furthest[shift(k - 1)] < furthest[shift(k + 1)]);
let mut x = if down { furthest[shift(k + 1)] } else { furthest[shift(k - 1)] + 1 };
let mut y = x - k;
while let (Ok(xi), Ok(yi)) = (usize::try_from(x), usize::try_from(y))
&& xi < n
&& yi < m
&& old[xi] == new[yi]
{
x += 1;
y += 1;
}
furthest[shift(k)] = x;
if usize::try_from(x).unwrap_or(0) >= n && usize::try_from(y).unwrap_or(0) >= m {
return walk_back(old, new, &rounds, d);
}
k += 2;
}
}
Vec::new()
}
fn walk_back<'a>(old: &[&'a str], new: &[&'a str], rounds: &[isize], d: isize) -> Vec<Step<'a>> {
let mut steps = Vec::new();
let mut x = isize::try_from(old.len()).unwrap_or(isize::MAX);
let mut y = isize::try_from(new.len()).unwrap_or(isize::MAX);
for round in (0..=d).rev() {
let reached = |k: isize| -> isize {
let index = round
.checked_mul(round)
.zip(k.checked_add(round))
.and_then(|(base, offset)| base.checked_add(offset));
index
.and_then(|index| usize::try_from(index).ok())
.and_then(|index| rounds.get(index).copied())
.unwrap_or(0)
};
let k = x - y;
let down = k == -round || (k != round && reached(k - 1) < reached(k + 1));
let previous = if down { k + 1 } else { k - 1 };
let start = reached(previous);
let (before_x, before_y) = (start, start - previous);
while x > before_x && y > before_y {
x -= 1;
y -= 1;
if let Some(line) = usize::try_from(x).ok().and_then(|index| old.get(index)) {
steps.push(Step::Kept(line));
}
}
if round == 0 {
break;
}
if down {
y -= 1;
if let Some(line) = usize::try_from(y).ok().and_then(|index| new.get(index)) {
steps.push(Step::Added(line));
}
} else {
x -= 1;
if let Some(line) = usize::try_from(x).ok().and_then(|index| old.get(index)) {
steps.push(Step::Removed(line));
}
}
}
steps.reverse();
steps
}