1use crate::structured::{DiffLineKind, FileDiff, FileDiffStatus};
6
7#[derive(Debug, Clone)]
9pub struct UnifiedDiffOptions {
10 pub a_prefix: String,
12 pub b_prefix: String,
14 pub color: bool,
16}
17
18impl Default for UnifiedDiffOptions {
19 fn default() -> Self {
20 Self {
21 a_prefix: "a/".into(),
22 b_prefix: "b/".into(),
23 color: false,
24 }
25 }
26}
27
28const ANSI_RESET: &str = "\x1b[0m";
29const ANSI_BOLD: &str = "\x1b[1m";
30const ANSI_RED: &str = "\x1b[31m";
31const ANSI_GREEN: &str = "\x1b[32m";
32const ANSI_CYAN: &str = "\x1b[36m";
33
34pub fn unified_diff_string(diff: &FileDiff, opts: &UnifiedDiffOptions) -> String {
38 if matches!(diff.status, FileDiffStatus::Unchanged) {
39 return String::new();
40 }
41
42 let mut s = String::new();
43 let a_path = diff.a_path.as_deref().unwrap_or("/dev/null");
44 let b_path = diff.b_path.as_deref().unwrap_or("/dev/null");
45
46 let header_color = if opts.color { ANSI_BOLD } else { "" };
47 let hunk_color = if opts.color { ANSI_CYAN } else { "" };
48 let add_color = if opts.color { ANSI_GREEN } else { "" };
49 let del_color = if opts.color { ANSI_RED } else { "" };
50 let reset = if opts.color { ANSI_RESET } else { "" };
51
52 s.push_str(&format!(
53 "{}diff --loom {}{}{}{}\n",
54 header_color,
55 opts.a_prefix,
56 a_path,
57 opts.b_prefix.is_empty().then(|| "").unwrap_or(""),
58 reset,
59 ));
60
61 match diff.status {
63 FileDiffStatus::Added => {
64 s.push_str(&format!("{}--- /dev/null{}\n", header_color, reset));
65 s.push_str(&format!(
66 "{}+++ {}{}{}\n",
67 header_color, opts.b_prefix, b_path, reset
68 ));
69 }
70 FileDiffStatus::Deleted => {
71 s.push_str(&format!(
72 "{}--- {}{}{}\n",
73 header_color, opts.a_prefix, a_path, reset
74 ));
75 s.push_str(&format!("{}+++ /dev/null{}\n", header_color, reset));
76 }
77 FileDiffStatus::Modified => {
78 s.push_str(&format!(
79 "{}--- {}{}{}\n",
80 header_color, opts.a_prefix, a_path, reset
81 ));
82 s.push_str(&format!(
83 "{}+++ {}{}{}\n",
84 header_color, opts.b_prefix, b_path, reset
85 ));
86 }
87 FileDiffStatus::Binary { ref reason } => {
88 s.push_str(&format!("Binary files differ ({:?})\n", reason));
89 return s;
90 }
91 FileDiffStatus::Unchanged => unreachable!(),
92 }
93
94 for hunk in &diff.hunks {
95 s.push_str(&format!(
96 "{}@@ -{},{} +{},{} @@{}\n",
97 hunk_color, hunk.a_start, hunk.a_count, hunk.b_start, hunk.b_count, reset,
98 ));
99 for line in &hunk.lines {
100 let (sigil, color) = match line.kind {
101 DiffLineKind::Delete => ('-', del_color),
102 DiffLineKind::Insert => ('+', add_color),
103 DiffLineKind::Equal => (' ', ""),
104 };
105 s.push_str(color);
106 s.push(sigil);
107 s.push_str(&line.content);
108 s.push_str(reset);
109 s.push('\n');
110 }
111 }
112
113 s
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::structured::file_diff;
120
121 #[test]
122 fn modified_renders_unified_with_hunk_header() {
123 let a = "alpha\nbeta\ngamma\n";
124 let b = "alpha\nBETA\ngamma\n";
125 let d = file_diff(Some(a), Some(b), Some("g.txt"), Some("g.txt"), 1);
126 let s = unified_diff_string(&d, &UnifiedDiffOptions::default());
127 assert!(s.contains("--- a/g.txt"));
128 assert!(s.contains("+++ b/g.txt"));
129 assert!(s.contains("@@ -"));
130 assert!(s.contains("-beta"));
131 assert!(s.contains("+BETA"));
132 }
133
134 #[test]
135 fn added_renders_dev_null_on_a_side() {
136 let d = file_diff(None, Some("hi\n"), None, Some("new.txt"), 3);
137 let s = unified_diff_string(&d, &UnifiedDiffOptions::default());
138 assert!(s.contains("--- /dev/null"));
139 assert!(s.contains("+++ b/new.txt"));
140 assert!(s.contains("+hi"));
141 }
142
143 #[test]
144 fn deleted_renders_dev_null_on_b_side() {
145 let d = file_diff(Some("bye\n"), None, Some("gone.txt"), None, 3);
146 let s = unified_diff_string(&d, &UnifiedDiffOptions::default());
147 assert!(s.contains("--- a/gone.txt"));
148 assert!(s.contains("+++ /dev/null"));
149 assert!(s.contains("-bye"));
150 }
151
152 #[test]
153 fn unchanged_renders_empty() {
154 let d = file_diff(Some("same\n"), Some("same\n"), Some("p"), Some("p"), 3);
155 let s = unified_diff_string(&d, &UnifiedDiffOptions::default());
156 assert!(s.is_empty());
157 }
158}