use harn_vm::text_diff::render_line_diff;
pub(super) struct FileDiff {
pub(super) text: String,
pub(super) lines_added: usize,
pub(super) lines_removed: usize,
}
pub(super) fn render(path: &str, before: &str, after: &str, kind: ChangeKind) -> FileDiff {
let diff = render_line_diff(before, after);
if diff.body.is_empty() {
return FileDiff {
text: String::new(),
lines_added: 0,
lines_removed: 0,
};
}
let header = match kind {
ChangeKind::Modify => format!("--- a/{path}\n+++ b/{path}\n"),
ChangeKind::Create => format!("--- /dev/null\n+++ b/{path}\n"),
ChangeKind::Delete => format!("--- a/{path}\n+++ /dev/null\n"),
};
FileDiff {
text: header + &diff.body,
lines_added: diff.lines_added,
lines_removed: diff.lines_removed,
}
}
#[derive(Clone, Copy)]
pub(super) enum ChangeKind {
Modify,
Create,
Delete,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_change_yields_empty_diff() {
let out = render(
"foo.txt",
"alpha\nbeta\n",
"alpha\nbeta\n",
ChangeKind::Modify,
);
assert!(out.text.is_empty());
assert_eq!(out.lines_added, 0);
assert_eq!(out.lines_removed, 0);
}
#[test]
fn single_line_modification_emits_hunk_header() {
let out = render(
"src/lib.rs",
"fn alpha() { 1 }\nfn beta() { 2 }\n",
"fn alpha() { 1 }\nfn beta() { 42 }\n",
ChangeKind::Modify,
);
assert!(out.text.contains("--- a/src/lib.rs"));
assert!(out.text.contains("+++ b/src/lib.rs"));
assert!(out.text.contains("@@ -"));
assert!(out.text.contains("-fn beta() { 2 }"));
assert!(out.text.contains("+fn beta() { 42 }"));
assert_eq!(out.lines_added, 1);
assert_eq!(out.lines_removed, 1);
}
#[test]
fn file_creation_uses_dev_null_a() {
let out = render("new.txt", "", "hello\n", ChangeKind::Create);
assert!(out.text.starts_with("--- /dev/null\n+++ b/new.txt\n"));
assert!(out.text.contains("+hello"));
assert_eq!(out.lines_added, 1);
assert_eq!(out.lines_removed, 0);
}
#[test]
fn file_deletion_uses_dev_null_b() {
let out = render("old.txt", "bye\n", "", ChangeKind::Delete);
assert!(out.text.starts_with("--- a/old.txt\n+++ /dev/null\n"));
assert!(out.text.contains("-bye"));
assert_eq!(out.lines_removed, 1);
assert_eq!(out.lines_added, 0);
}
#[test]
fn missing_trailing_newline_emits_marker() {
let out = render("f.txt", "alpha\nbeta", "alpha\nbeta\n", ChangeKind::Modify);
assert!(out.text.contains("-beta\n\\ No newline at end of file\n"));
}
#[test]
fn hunk_with_context_marks_three_surrounding_lines() {
let before = "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n";
let after = "a\nb\nc\nD\ne\nf\ng\nh\ni\nj\n";
let out = render("f.txt", before, after, ChangeKind::Modify);
let body = out.text.lines().collect::<Vec<_>>();
assert!(body.iter().any(|l| l.starts_with(" a")));
assert!(body.iter().any(|l| l.starts_with(" g")));
assert!(!body.iter().any(|l| l.starts_with(" j")));
assert!(body.contains(&"-d"));
assert!(body.contains(&"+D"));
}
#[test]
fn distant_changes_produce_separate_hunks() {
let before = "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\n";
let after = "a\nB\nc\nd\ne\nf\ng\nh\ni\nj\nk\nL\n";
let out = render("f.txt", before, after, ChangeKind::Modify);
let hunk_headers = out.text.matches("@@ -").count();
assert_eq!(hunk_headers, 2);
}
}