Skip to main content

zagens_runtime_adapters/tools/
diff_format.rs

1//! Build unified-diff strings for tool results.
2//!
3//! `edit_file` and `write_file` capture the file contents before and after
4//! the mutation and emit a unified diff at the head of their `ToolResult`
5//! output. The TUI's `output_looks_like_diff` detector then routes the
6//! payload through `diff_render::render_diff`, which renders it with line
7//! numbers and coloured `+`/`-` gutters (#505).
8
9use similar::TextDiff;
10
11/// Build a unified diff between `old` and `new` keyed at `path`.
12///
13/// Returns an empty string when the inputs are byte-identical so callers
14/// can skip the "no changes" header. The output uses git-style `--- a/...`
15/// / `+++ b/...` headers and three lines of context — matching the format
16/// the TUI's `diff_render::render_diff` already understands.
17#[must_use]
18pub fn make_unified_diff(path: &str, old: &str, new: &str) -> String {
19    if old == new {
20        return String::new();
21    }
22    let a = format!("a/{path}");
23    let b = format!("b/{path}");
24    let diff = TextDiff::from_lines(old, new);
25    diff.unified_diff()
26        .context_radius(3)
27        .header(&a, &b)
28        .to_string()
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn identical_inputs_emit_empty_diff() {
37        let s = "hello\nworld\n";
38        assert!(make_unified_diff("foo.txt", s, s).is_empty());
39    }
40
41    #[test]
42    fn replacement_emits_minus_plus_pair() {
43        let old = "alpha\nbeta\ngamma\n";
44        let new = "alpha\nBETA\ngamma\n";
45        let diff = make_unified_diff("foo.txt", old, new);
46        assert!(diff.contains("--- a/foo.txt"), "{diff}");
47        assert!(diff.contains("+++ b/foo.txt"), "{diff}");
48        assert!(diff.contains("-beta"), "{diff}");
49        assert!(diff.contains("+BETA"), "{diff}");
50    }
51
52    #[test]
53    fn new_file_renders_against_empty_old() {
54        let new = "first line\nsecond line\n";
55        let diff = make_unified_diff("new.txt", "", new);
56        assert!(diff.contains("--- a/new.txt"), "{diff}");
57        assert!(diff.contains("+++ b/new.txt"), "{diff}");
58        assert!(diff.contains("+first line"), "{diff}");
59        assert!(diff.contains("+second line"), "{diff}");
60    }
61
62    #[test]
63    fn diff_contains_hunk_header_so_tui_renders_it() {
64        let diff = make_unified_diff("foo.txt", "a\n", "b\n");
65        let head: Vec<&str> = diff.lines().take(5).collect();
66        assert!(
67            head.iter().any(|line| line.starts_with("@@")),
68            "expected hunk header in first 5 lines; got {head:?}"
69        );
70    }
71}