use std::fmt::Write;
pub fn unified_diff(path: &str, old: &str, new: &str) -> (String, usize) {
let old_lines: Vec<&str> = old.lines().collect();
let new_lines: Vec<&str> = new.lines().collect();
let lcs = lcs_table(&old_lines, &new_lines);
let edits = backtrack(&lcs, &old_lines, &new_lines);
let hunks = group_hunks(&edits, 3);
let mut out = String::new();
let _ = writeln!(out, "--- a/{path}");
let _ = writeln!(out, "+++ b/{path}");
let mut lines_changed = 0usize;
for hunk in &hunks {
let (old_start, old_count, new_start, new_count) = hunk_header(hunk);
let _ = writeln!(
out,
"@@ -{},{} +{},{} @@",
old_start, old_count, new_start, new_count
);
for edit in hunk {
match edit {
Edit::Keep(line) => {
let _ = writeln!(out, " {line}");
}
Edit::Delete(line) => {
let _ = writeln!(out, "-{line}");
lines_changed += 1;
}
Edit::Insert(line) => {
let _ = writeln!(out, "+{line}");
lines_changed += 1;
}
}
}
}
(out, lines_changed)
}
#[derive(Debug)]
enum Edit<'a> {
Keep(&'a str),
Delete(&'a str),
Insert(&'a str),
}
fn lcs_table(old: &[&str], new: &[&str]) -> Vec<Vec<usize>> {
let m = old.len();
let n = new.len();
let mut table = vec![vec![0usize; n + 1]; m + 1];
for i in 1..=m {
for j in 1..=n {
if old[i - 1] == new[j - 1] {
table[i][j] = table[i - 1][j - 1] + 1;
} else {
table[i][j] = table[i - 1][j].max(table[i][j - 1]);
}
}
}
table
}
fn backtrack<'a>(table: &[Vec<usize>], old: &[&'a str], new: &[&'a str]) -> Vec<Edit<'a>> {
let mut edits = Vec::new();
let mut i = old.len();
let mut j = new.len();
while i > 0 || j > 0 {
if i > 0 && j > 0 && old[i - 1] == new[j - 1] {
edits.push(Edit::Keep(old[i - 1]));
i -= 1;
j -= 1;
} else if j > 0 && (i == 0 || table[i][j - 1] >= table[i - 1][j]) {
edits.push(Edit::Insert(new[j - 1]));
j -= 1;
} else {
edits.push(Edit::Delete(old[i - 1]));
i -= 1;
}
}
edits.reverse();
edits
}
fn group_hunks<'a>(edits: &[Edit<'a>], context: usize) -> Vec<Vec<Edit<'a>>> {
if edits.is_empty() {
return Vec::new();
}
let changed: Vec<usize> = edits
.iter()
.enumerate()
.filter(|(_, e)| !matches!(e, Edit::Keep(_)))
.map(|(i, _)| i)
.collect();
if changed.is_empty() {
return Vec::new();
}
let mut hunks = Vec::new();
let mut hunk_start = changed[0].saturating_sub(context);
let mut hunk_end = (changed[0] + context + 1).min(edits.len());
for &idx in &changed[1..] {
let start = idx.saturating_sub(context);
let end = (idx + context + 1).min(edits.len());
if start <= hunk_end {
hunk_end = end;
} else {
hunks.push(edits[hunk_start..hunk_end].to_vec());
hunk_start = start;
hunk_end = end;
}
}
hunks.push(edits[hunk_start..hunk_end].to_vec());
hunks
}
fn hunk_header(hunk: &[Edit<'_>]) -> (usize, usize, usize, usize) {
let mut old_start = 1usize;
let mut old_count = 0usize;
let mut new_start = 1usize;
let mut new_count = 0usize;
for edit in hunk {
match edit {
Edit::Keep(_) => {
old_count += 1;
new_count += 1;
}
Edit::Delete(_) => {
old_count += 1;
}
Edit::Insert(_) => {
new_count += 1;
}
}
}
let _ = (&mut old_start, &mut new_start);
(old_start, old_count, new_start, new_count)
}
impl Clone for Edit<'_> {
fn clone(&self) -> Self {
match self {
Edit::Keep(s) => Edit::Keep(s),
Edit::Delete(s) => Edit::Delete(s),
Edit::Insert(s) => Edit::Insert(s),
}
}
}