返回 DeepSeek-TUI-2026
diff_format.rs
根目录 / crates / tui / src / 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 //!
9 //! The diff is also a strict UX upgrade for the model — it sees exactly
10 //! which lines changed instead of a one-line summary.
11
12 use similar::TextDiff;
13
14 /// Build a unified diff between `old` and `new` keyed at `path`.
15 ///
16 /// Returns an empty string when the inputs are byte-identical so callers
17 /// can skip the "no changes" header. The output uses git-style `--- a/...`
18 /// / `+++ b/...` headers and three lines of context — matching the format
19 /// the TUI's `diff_render::render_diff` already understands.
20 #[must_use]
21 pub fn make_unified_diff(path: &str, old: &str, new: &str) -> String {
22 if old == new {
23 return String::new();
24 }
25 let a = format!("a/{path}");
26 let b = format!("b/{path}");
27 let diff = TextDiff::from_lines(old, new);
28 diff.unified_diff()
29 .context_radius(3)
30 .header(&a, &b)
31 .to_string()
32 }
33
34 #[cfg(test)]
35 mod tests {
36 use super::*;
37
38 #[test]
39 fn identical_inputs_emit_empty_diff() {
40 let s = "hello\nworld\n";
41 assert!(make_unified_diff("foo.txt", s, s).is_empty());
42 }
43
44 #[test]
45 fn replacement_emits_minus_plus_pair() {
46 let old = "alpha\nbeta\ngamma\n";
47 let new = "alpha\nBETA\ngamma\n";
48 let diff = make_unified_diff("foo.txt", old, new);
49 assert!(diff.contains("--- a/foo.txt"), "{diff}");
50 assert!(diff.contains("+++ b/foo.txt"), "{diff}");
51 assert!(diff.contains("-beta"), "{diff}");
52 assert!(diff.contains("+BETA"), "{diff}");
53 }
54
55 #[test]
56 fn new_file_renders_against_empty_old() {
57 let new = "first line\nsecond line\n";
58 let diff = make_unified_diff("new.txt", "", new);
59 assert!(diff.contains("--- a/new.txt"), "{diff}");
60 assert!(diff.contains("+++ b/new.txt"), "{diff}");
61 assert!(diff.contains("+first line"), "{diff}");
62 assert!(diff.contains("+second line"), "{diff}");
63 }
64
65 #[test]
66 fn diff_contains_hunk_header_so_tui_renders_it() {
67 // The TUI detector scans the first 5 lines for `@@`. Make sure the
68 // unified diff puts a hunk header within that window so the
69 // diff-aware renderer kicks in (#505).
70 let diff = make_unified_diff("foo.txt", "a\n", "b\n");
71 let head: Vec<&str> = diff.lines().take(5).collect();
72 assert!(
73 head.iter().any(|line| line.starts_with("@@")),
74 "expected hunk header in first 5 lines; got {head:?}"
75 );
76 }
77 }
78
78 lines RUST