1use anstyle::{AnsiColor, Color, Reset, Style};
8use std::fmt::Write;
9
10pub use vtcode_commons::diff::{
12 Chunk, DiffBundle, DiffHunk, DiffLine, DiffLineKind, DiffOptions, compute_diff,
13 compute_diff_chunks,
14};
15
16pub fn format_unified_diff(old: &str, new: &str, options: DiffOptions<'_>) -> String {
18 let mut options = options;
19 options.missing_newline_hint = false;
20 let bundle = compute_diff(old, new, options, format_colored_diff);
21 vtcode_commons::ansi::strip_ansi(&bundle.formatted)
22}
23
24pub fn compute_diff_with_theme(old: &str, new: &str, options: DiffOptions<'_>) -> DiffBundle {
26 compute_diff(old, new, options, format_colored_diff)
27}
28
29pub fn format_colored_diff(hunks: &[DiffHunk], options: &DiffOptions<'_>) -> String {
34 if hunks.is_empty() {
35 return String::new();
36 }
37
38 let cyan_style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
39 let addition_style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)));
40 let deletion_style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)));
41 let context_style = Style::new();
42
43 let mut output = String::new();
44
45 if let (Some(old_label), Some(new_label)) = (options.old_label, options.new_label) {
46 let _ = write!(
47 output,
48 "{}--- {old_label}\n{}",
49 cyan_style.render(),
50 Reset.render()
51 );
52
53 let _ = write!(
54 output,
55 "{}+++ {new_label}\n{}",
56 cyan_style.render(),
57 Reset.render()
58 );
59 }
60
61 for hunk in hunks {
62 let _ = write!(
63 output,
64 "{}@@ -{},{} +{},{} @@\n{}",
65 cyan_style.render(),
66 hunk.old_start,
67 hunk.old_lines,
68 hunk.new_start,
69 hunk.new_lines,
70 Reset.render()
71 );
72
73 for line in &hunk.lines {
74 let (style, prefix) = match line.kind {
75 DiffLineKind::Addition => (&addition_style, '+'),
76 DiffLineKind::Deletion => (&deletion_style, '-'),
77 DiffLineKind::Context => (&context_style, ' '),
78 };
79
80 let mut display = String::with_capacity(line.text.len() + 2);
81 display.push(prefix);
82 display.push_str(&line.text);
83
84 let has_newline = display.ends_with('\n');
86 let display_content = if has_newline {
87 &display[..display.len() - 1]
88 } else {
89 &display
90 };
91
92 let _ = write!(
93 output,
94 "{}{} {}",
95 style.render(),
96 display_content,
97 Reset.render()
98 );
99 output.push('\n');
100
101 if options.missing_newline_hint && !line.text.ends_with('\n') {
102 let eof_hint = r"\ No newline at end of file";
103 let _ = write!(
104 output,
105 "{}{} {}",
106 context_style.render(),
107 eof_hint,
108 Reset.render()
109 );
110 output.push('\n');
111 }
112 }
113 }
114
115 output
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn computes_structured_diff() {
124 let before = "a\nb\nc\n";
125 let after = "a\nc\nd\n";
126 let bundle = compute_diff(
127 before,
128 after,
129 DiffOptions {
130 context_lines: 2,
131 old_label: Some("old"),
132 new_label: Some("new"),
133 ..Default::default()
134 },
135 format_colored_diff,
136 );
137
138 assert!(!bundle.is_empty);
139 assert_eq!(bundle.hunks.len(), 1);
140 let hunk = &bundle.hunks[0];
141 assert_eq!(hunk.old_start, 1);
142 assert_eq!(hunk.new_start, 1);
143 assert!(bundle.formatted.contains("@@"));
144 assert!(
145 hunk.lines
146 .iter()
147 .any(|line| matches!(line.kind, DiffLineKind::Deletion))
148 );
149 assert!(
150 hunk.lines
151 .iter()
152 .any(|line| matches!(line.kind, DiffLineKind::Addition))
153 );
154 }
155
156 #[test]
157 fn empty_hunks_returns_empty_string() {
158 let result = format_colored_diff(&[], &DiffOptions::default());
159 assert!(result.is_empty());
160 }
161
162 #[test]
163 fn format_unified_diff_has_no_ansi() {
164 let before = "hello\n";
165 let after = "world\n";
166 let result = format_unified_diff(before, after, DiffOptions::default());
167 assert!(!result.contains('\x1b'));
169 assert!(result.contains('-') || result.contains('+'));
171 }
172}