1use anstyle::{Reset, Style};
2use anstyle_git::parse as parse_git_style;
3use std::path::Path;
4
5struct GitDiffPalette {
6 bullet: Style,
7 label: Style,
8 path: Style,
9 stat_added: Style,
10 stat_removed: Style,
11 line_added: Style,
12 line_removed: Style,
13 line_context: Style,
14 line_header: Style,
15 line_number: Style,
16}
17
18impl GitDiffPalette {
19 fn new(use_colors: bool) -> Self {
20 let parse = |spec: &str| -> Style {
21 if use_colors {
22 parse_git_style(spec).unwrap_or_else(|_| Style::new())
23 } else {
24 Style::new()
25 }
26 };
27
28 Self {
29 bullet: parse("bold yellow"),
30 label: parse("bold white"),
31 path: parse("bold"),
32 stat_added: parse("bold green"),
33 stat_removed: parse("bold red"),
34 line_added: parse("green"),
35 line_removed: parse("red"),
36 line_context: parse("dim"),
37 line_header: parse("bold yellow"),
38 line_number: parse("dim"),
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
44pub struct DiffLine {
45 pub line_type: DiffLineType,
46 pub content: String,
47 pub line_number_old: Option<usize>,
48 pub line_number_new: Option<usize>,
49}
50
51#[derive(Debug, Clone, PartialEq)]
52pub enum DiffLineType {
53 Added,
54 Removed,
55 Context,
56 Header,
57}
58
59#[derive(Debug)]
60pub struct FileDiff {
61 pub file_path: String,
62 pub old_content: String,
63 pub new_content: String,
64 pub lines: Vec<DiffLine>,
65 pub stats: DiffStats,
66}
67
68#[derive(Debug)]
69pub struct DiffStats {
70 pub additions: usize,
71 pub deletions: usize,
72 pub changes: usize,
73}
74
75pub struct DiffRenderer {
76 show_line_numbers: bool,
77 context_lines: usize,
78 use_colors: bool,
79 palette: GitDiffPalette,
80}
81
82impl DiffRenderer {
83 pub fn new(show_line_numbers: bool, context_lines: usize, use_colors: bool) -> Self {
84 Self {
85 show_line_numbers,
86 context_lines,
87 use_colors,
88 palette: GitDiffPalette::new(use_colors),
89 }
90 }
91
92 pub fn render_diff(&self, diff: &FileDiff) -> String {
93 let mut output = String::new();
94 output.push_str(&self.render_summary(diff));
95 output.push('\n');
96
97 for line in &diff.lines {
98 output.push_str(&self.render_line(line));
99 output.push('\n');
100 }
101
102 output
103 }
104
105 fn render_summary(&self, diff: &FileDiff) -> String {
106 let bullet = self.paint(&self.palette.bullet, "•");
107 let label = self.paint(&self.palette.label, "Edited");
108 let path = self.paint(&self.palette.path, &diff.file_path);
109 let additions = format!("+{}", diff.stats.additions);
110 let deletions = format!("-{}", diff.stats.deletions);
111 let added_span = self.paint(&self.palette.stat_added, &additions);
112 let removed_span = self.paint(&self.palette.stat_removed, &deletions);
113 format!("{bullet} {label} {path} ({added_span} {removed_span})")
114 }
115
116 fn render_line(&self, line: &DiffLine) -> String {
117 let (style, prefix, line_number) = match line.line_type {
118 DiffLineType::Added => (&self.palette.line_added, "+", line.line_number_new),
119 DiffLineType::Removed => (&self.palette.line_removed, "-", line.line_number_old),
120 DiffLineType::Context => (
121 &self.palette.line_context,
122 " ",
123 line.line_number_new.or(line.line_number_old),
124 ),
125 DiffLineType::Header => (&self.palette.line_header, "", None),
126 };
127
128 let mut rendered = String::new();
129
130 if self.show_line_numbers {
131 let number_text = line_number
132 .map(|n| format!("{:>4}", n))
133 .unwrap_or_else(|| " ".to_string());
134 rendered.push_str(&self.paint(&self.palette.line_number, &format!("{} ", number_text)));
135 }
136
137 let content = match line.line_type {
138 DiffLineType::Header => line.content.clone(),
139 DiffLineType::Context => format!("{}{}", prefix, line.content),
140 _ => {
141 if line.content.is_empty() {
142 prefix.to_string()
143 } else {
144 format!("{prefix} {}", line.content)
145 }
146 }
147 };
148
149 rendered.push_str(&self.paint(style, &content));
150 rendered
151 }
152
153 fn paint(&self, style: &Style, text: &str) -> String {
154 if self.use_colors {
155 format!("{style}{text}{Reset}")
156 } else {
157 text.to_string()
158 }
159 }
160
161 pub fn generate_diff(&self, old_content: &str, new_content: &str, file_path: &str) -> FileDiff {
162 let old_lines: Vec<&str> = old_content.lines().collect();
163 let new_lines: Vec<&str> = new_content.lines().collect();
164
165 let mut lines = Vec::new();
166 let mut additions = 0;
167 let mut deletions = 0;
168 let _changes = 0;
169
170 let mut old_idx = 0;
172 let mut new_idx = 0;
173
174 while old_idx < old_lines.len() || new_idx < new_lines.len() {
175 if old_idx < old_lines.len() && new_idx < new_lines.len() {
176 if old_lines[old_idx] == new_lines[new_idx] {
177 lines.push(DiffLine {
179 line_type: DiffLineType::Context,
180 content: old_lines[old_idx].to_string(),
181 line_number_old: Some(old_idx + 1),
182 line_number_new: Some(new_idx + 1),
183 });
184 old_idx += 1;
185 new_idx += 1;
186 } else {
187 let (old_end, new_end) =
189 self.find_difference(&old_lines, &new_lines, old_idx, new_idx);
190
191 for i in old_idx..old_end {
193 lines.push(DiffLine {
194 line_type: DiffLineType::Removed,
195 content: old_lines[i].to_string(),
196 line_number_old: Some(i + 1),
197 line_number_new: None,
198 });
199 deletions += 1;
200 }
201
202 for i in new_idx..new_end {
204 lines.push(DiffLine {
205 line_type: DiffLineType::Added,
206 content: new_lines[i].to_string(),
207 line_number_old: None,
208 line_number_new: Some(i + 1),
209 });
210 additions += 1;
211 }
212
213 old_idx = old_end;
214 new_idx = new_end;
215 }
216 } else if old_idx < old_lines.len() {
217 lines.push(DiffLine {
219 line_type: DiffLineType::Removed,
220 content: old_lines[old_idx].to_string(),
221 line_number_old: Some(old_idx + 1),
222 line_number_new: None,
223 });
224 deletions += 1;
225 old_idx += 1;
226 } else if new_idx < new_lines.len() {
227 lines.push(DiffLine {
229 line_type: DiffLineType::Added,
230 content: new_lines[new_idx].to_string(),
231 line_number_old: None,
232 line_number_new: Some(new_idx + 1),
233 });
234 additions += 1;
235 new_idx += 1;
236 }
237 }
238
239 let changes = additions + deletions;
240
241 FileDiff {
242 file_path: file_path.to_string(),
243 old_content: old_content.to_string(),
244 new_content: new_content.to_string(),
245 lines,
246 stats: DiffStats {
247 additions,
248 deletions,
249 changes,
250 },
251 }
252 }
253
254 fn find_difference(
255 &self,
256 old_lines: &[&str],
257 new_lines: &[&str],
258 start_old: usize,
259 start_new: usize,
260 ) -> (usize, usize) {
261 let mut old_end = start_old;
262 let mut new_end = start_new;
263
264 while old_end < old_lines.len() && new_end < new_lines.len() {
266 if old_lines[old_end] == new_lines[new_end] {
267 return (old_end, new_end);
268 }
269
270 let mut found = false;
272 for i in 1..=self.context_lines {
273 if old_end + i < old_lines.len() && new_end + i < new_lines.len() {
274 if old_lines[old_end + i] == new_lines[new_end + i] {
275 old_end += i;
276 new_end += i;
277 found = true;
278 break;
279 }
280 }
281 }
282
283 if !found {
284 old_end += 1;
285 new_end += 1;
286 }
287 }
288
289 (old_end, new_end)
290 }
291}
292
293pub struct DiffChatRenderer {
294 diff_renderer: DiffRenderer,
295}
296
297impl DiffChatRenderer {
298 pub fn new(show_line_numbers: bool, context_lines: usize, use_colors: bool) -> Self {
299 Self {
300 diff_renderer: DiffRenderer::new(show_line_numbers, context_lines, use_colors),
301 }
302 }
303
304 pub fn render_file_change(
305 &self,
306 file_path: &Path,
307 old_content: &str,
308 new_content: &str,
309 ) -> String {
310 let diff = self.diff_renderer.generate_diff(
311 old_content,
312 new_content,
313 &file_path.to_string_lossy(),
314 );
315 self.diff_renderer.render_diff(&diff)
316 }
317
318 pub fn render_multiple_changes(&self, changes: Vec<(String, String, String)>) -> String {
319 let mut output = format!("\nMultiple File Changes ({} files)\n", changes.len());
320 output.push_str("═".repeat(60).as_str());
321 output.push_str("\n\n");
322
323 for (file_path, old_content, new_content) in changes {
324 let diff = self
325 .diff_renderer
326 .generate_diff(&old_content, &new_content, &file_path);
327 output.push_str(&self.diff_renderer.render_diff(&diff));
328 }
329
330 output
331 }
332
333 pub fn render_operation_summary(
334 &self,
335 operation: &str,
336 files_affected: usize,
337 success: bool,
338 ) -> String {
339 let status = if success { "[Success]" } else { "[Failure]" };
340 let mut summary = format!("\n{} {}\n", status, operation);
341 summary.push_str(&format!(" Files affected: {}\n", files_affected));
342
343 if success {
344 summary.push_str("Operation completed successfully!\n");
345 } else {
346 summary.push_str(" Operation completed with errors\n");
347 }
348
349 summary
350 }
351}
352
353pub fn generate_unified_diff(old_content: &str, new_content: &str, filename: &str) -> String {
354 let mut diff = format!("--- a/{}\n+++ b/{}\n", filename, filename);
355
356 let old_lines: Vec<&str> = old_content.lines().collect();
357 let new_lines: Vec<&str> = new_content.lines().collect();
358
359 let mut old_idx = 0;
360 let mut new_idx = 0;
361
362 while old_idx < old_lines.len() || new_idx < new_lines.len() {
363 let start_old = old_idx;
365 let start_new = new_idx;
366
367 while old_idx < old_lines.len()
369 && new_idx < new_lines.len()
370 && old_lines[old_idx] == new_lines[new_idx]
371 {
372 old_idx += 1;
373 new_idx += 1;
374 }
375
376 if old_idx == old_lines.len() && new_idx == new_lines.len() {
377 break; }
379
380 let mut end_old = old_idx;
382 let mut end_new = new_idx;
383
384 let mut context_found = false;
386 for i in 0..3 {
387 if end_old + i < old_lines.len() && end_new + i < new_lines.len() {
389 if old_lines[end_old + i] == new_lines[end_new + i] {
390 end_old += i;
391 end_new += i;
392 context_found = true;
393 break;
394 }
395 }
396 }
397
398 if !context_found {
399 end_old = old_lines.len();
400 end_new = new_lines.len();
401 }
402
403 let old_count = end_old - start_old;
405 let new_count = end_new - start_new;
406
407 diff.push_str(&format!(
408 "@@ -{},{} +{},{} @@\n",
409 start_old + 1,
410 old_count,
411 start_new + 1,
412 new_count
413 ));
414
415 for i in (start_old.saturating_sub(3))..start_old {
417 if i < old_lines.len() {
418 diff.push_str(&format!(" {}\n", old_lines[i]));
419 }
420 }
421
422 for i in start_old..end_old {
424 if i < old_lines.len() {
425 diff.push_str(&format!("-{}\n", old_lines[i]));
426 }
427 }
428
429 for i in start_new..end_new {
431 if i < new_lines.len() {
432 diff.push_str(&format!("+{}\n", new_lines[i]));
433 }
434 }
435
436 for i in end_old..(end_old + 3) {
438 if i < old_lines.len() {
439 diff.push_str(&format!(" {}\n", old_lines[i]));
440 }
441 }
442
443 old_idx = end_old;
444 new_idx = end_new;
445 }
446
447 diff
448}