1use console::Style;
2use std::path::Path;
3
4pub struct DiffStyles;
6
7impl DiffStyles {
8 pub fn file_header() -> Style {
10 Style::new().bold().blue()
11 }
12
13 pub fn file_path() -> Style {
15 Style::new().bold().cyan()
16 }
17
18 pub fn stats_header() -> Style {
20 Style::new().bold().magenta()
21 }
22
23 pub fn additions_count() -> Style {
25 Style::new().bold().green()
26 }
27
28 pub fn deletions_count() -> Style {
30 Style::new().bold().red()
31 }
32
33 pub fn changes_count() -> Style {
35 Style::new().bold().yellow()
36 }
37
38 pub fn summary_header() -> Style {
40 Style::new().bold().cyan()
41 }
42
43 pub fn added_line() -> Style {
45 Style::new().green()
46 }
47
48 pub fn removed_line() -> Style {
50 Style::new().red()
51 }
52
53 pub fn context_line() -> Style {
55 Style::new().white().dim()
56 }
57
58 pub fn header_line() -> Style {
60 Style::new().bold().blue()
61 }
62
63 pub fn apply_style(style: &Style, text: &str) -> String {
65 style.apply_to(text).to_string()
66 }
67}
68
69#[derive(Debug, Clone)]
70pub struct DiffLine {
71 pub line_type: DiffLineType,
72 pub content: String,
73 pub line_number_old: Option<usize>,
74 pub line_number_new: Option<usize>,
75}
76
77#[derive(Debug, Clone, PartialEq)]
78pub enum DiffLineType {
79 Added,
80 Removed,
81 Context,
82 Header,
83}
84
85#[derive(Debug)]
86pub struct FileDiff {
87 pub file_path: String,
88 pub old_content: String,
89 pub new_content: String,
90 pub lines: Vec<DiffLine>,
91 pub stats: DiffStats,
92}
93
94#[derive(Debug)]
95pub struct DiffStats {
96 pub additions: usize,
97 pub deletions: usize,
98 pub changes: usize,
99}
100
101pub struct DiffRenderer {
102 show_line_numbers: bool,
103 context_lines: usize,
104 use_colors: bool,
105}
106
107impl DiffRenderer {
108 pub fn new(show_line_numbers: bool, context_lines: usize, use_colors: bool) -> Self {
109 Self {
110 show_line_numbers,
111 context_lines,
112 use_colors,
113 }
114 }
115
116 pub fn render_diff(&self, diff: &FileDiff) -> String {
117 let mut output = String::new();
118
119 output.push_str(&self.render_header(&diff.file_path, &diff.stats));
121
122 for line in &diff.lines {
124 output.push_str(&self.render_line(line));
125 output.push('\n');
126 }
127
128 output.push_str(&self.render_footer(&diff.stats));
130
131 output
132 }
133
134 fn render_header(&self, file_path: &str, stats: &DiffStats) -> String {
135 let file_header_style = if self.use_colors {
136 DiffStyles::file_header()
137 } else {
138 Style::new()
139 };
140 let file_path_style = if self.use_colors {
141 DiffStyles::file_path()
142 } else {
143 Style::new()
144 };
145
146 let mut header = format!(
147 "\n{}{} File: {}{}\n",
148 file_header_style.apply_to("FILE"),
149 if self.use_colors { "\x1b[0m" } else { "" },
150 file_path_style.apply_to(file_path),
151 if self.use_colors { "\x1b[0m" } else { "" }
152 );
153
154 let stats_header_style = if self.use_colors {
155 DiffStyles::stats_header()
156 } else {
157 Style::new()
158 };
159 let additions_style = if self.use_colors {
160 DiffStyles::additions_count()
161 } else {
162 Style::new()
163 };
164 let deletions_style = if self.use_colors {
165 DiffStyles::deletions_count()
166 } else {
167 Style::new()
168 };
169 let changes_style = if self.use_colors {
170 DiffStyles::changes_count()
171 } else {
172 Style::new()
173 };
174
175 header.push_str(&format!(
176 "{}{} Changes: {}{} additions, {}{} deletions, {}{} modifications\n",
177 stats_header_style.apply_to("STATS"),
178 if self.use_colors { "\x1b[0m" } else { "" },
179 additions_style.apply_to(&stats.additions.to_string()),
180 if self.use_colors { "\x1b[0m" } else { "" },
181 deletions_style.apply_to(&stats.deletions.to_string()),
182 if self.use_colors { "\x1b[0m" } else { "" },
183 changes_style.apply_to(&stats.changes.to_string()),
184 if self.use_colors { "\x1b[0m" } else { "" }
185 ));
186
187 if self.show_line_numbers {
188 header.push_str("┌─────┬─────────────────────────────────────────────────\n");
189 } else {
190 header.push_str("┌───────────────────────────────────────────────────────\n");
191 }
192
193 header
194 }
195
196 fn render_line(&self, line: &DiffLine) -> String {
197 let prefix = match line.line_type {
198 DiffLineType::Added => "+",
199 DiffLineType::Removed => "-",
200 DiffLineType::Context => " ",
201 DiffLineType::Header => "@",
202 };
203
204 let style = match line.line_type {
205 DiffLineType::Added => DiffStyles::added_line(),
206 DiffLineType::Removed => DiffStyles::removed_line(),
207 DiffLineType::Context => DiffStyles::context_line(),
208 DiffLineType::Header => DiffStyles::header_line(),
209 };
210
211 let mut result = String::new();
212
213 if self.show_line_numbers {
214 let old_num = line
215 .line_number_old
216 .map_or("".to_string(), |n| format!("{:4}", n));
217 let new_num = line
218 .line_number_new
219 .map_or("".to_string(), |n| format!("{:4}", n));
220 result.push_str(&format!("│{}/{}│", old_num, new_num));
221 }
222
223 if self.use_colors {
224 let styled_prefix = self.colorize(prefix, &style);
225 let styled_content = self.colorize(&line.content, &style);
226 result.push_str(&format!("{}{}", styled_prefix, styled_content));
227 } else {
228 result.push_str(&format!("{}{}", prefix, line.content));
229 }
230
231 result
232 }
233
234 fn render_footer(&self, stats: &DiffStats) -> String {
235 let mut footer = String::new();
236
237 if self.show_line_numbers {
238 footer.push_str("└─────┴─────────────────────────────────────────────────\n");
239 } else {
240 footer.push_str("└───────────────────────────────────────────────────────\n");
241 }
242
243 let summary_header_style = if self.use_colors {
244 DiffStyles::summary_header()
245 } else {
246 Style::new()
247 };
248 let summary_additions_style = if self.use_colors {
249 DiffStyles::additions_count()
250 } else {
251 Style::new()
252 };
253 let summary_deletions_style = if self.use_colors {
254 DiffStyles::deletions_count()
255 } else {
256 Style::new()
257 };
258 let summary_changes_style = if self.use_colors {
259 DiffStyles::changes_count()
260 } else {
261 Style::new()
262 };
263
264 footer.push_str(&format!(
265 "{}{} Summary: {}{} lines added, {}{} lines removed, {}{} lines changed\n\n",
266 summary_header_style.apply_to("SUMMARY"),
267 if self.use_colors { "\x1b[0m" } else { "" },
268 summary_additions_style.apply_to(&stats.additions.to_string()),
269 if self.use_colors { "\x1b[0m" } else { "" },
270 summary_deletions_style.apply_to(&stats.deletions.to_string()),
271 if self.use_colors { "\x1b[0m" } else { "" },
272 summary_changes_style.apply_to(&stats.changes.to_string()),
273 if self.use_colors { "\x1b[0m" } else { "" }
274 ));
275
276 footer
277 }
278
279 fn colorize(&self, text: &str, style: &Style) -> String {
280 if self.use_colors {
281 DiffStyles::apply_style(style, text)
282 } else {
283 text.to_string()
284 }
285 }
286
287 pub fn generate_diff(&self, old_content: &str, new_content: &str, file_path: &str) -> FileDiff {
288 let old_lines: Vec<&str> = old_content.lines().collect();
289 let new_lines: Vec<&str> = new_content.lines().collect();
290
291 let mut lines = Vec::new();
292 let mut additions = 0;
293 let mut deletions = 0;
294 let _changes = 0;
295
296 let mut old_idx = 0;
298 let mut new_idx = 0;
299
300 while old_idx < old_lines.len() || new_idx < new_lines.len() {
301 if old_idx < old_lines.len() && new_idx < new_lines.len() {
302 if old_lines[old_idx] == new_lines[new_idx] {
303 lines.push(DiffLine {
305 line_type: DiffLineType::Context,
306 content: old_lines[old_idx].to_string(),
307 line_number_old: Some(old_idx + 1),
308 line_number_new: Some(new_idx + 1),
309 });
310 old_idx += 1;
311 new_idx += 1;
312 } else {
313 let (old_end, new_end) =
315 self.find_difference(&old_lines, &new_lines, old_idx, new_idx);
316
317 for i in old_idx..old_end {
319 lines.push(DiffLine {
320 line_type: DiffLineType::Removed,
321 content: old_lines[i].to_string(),
322 line_number_old: Some(i + 1),
323 line_number_new: None,
324 });
325 deletions += 1;
326 }
327
328 for i in new_idx..new_end {
330 lines.push(DiffLine {
331 line_type: DiffLineType::Added,
332 content: new_lines[i].to_string(),
333 line_number_old: None,
334 line_number_new: Some(i + 1),
335 });
336 additions += 1;
337 }
338
339 old_idx = old_end;
340 new_idx = new_end;
341 }
342 } else if old_idx < old_lines.len() {
343 lines.push(DiffLine {
345 line_type: DiffLineType::Removed,
346 content: old_lines[old_idx].to_string(),
347 line_number_old: Some(old_idx + 1),
348 line_number_new: None,
349 });
350 deletions += 1;
351 old_idx += 1;
352 } else if new_idx < new_lines.len() {
353 lines.push(DiffLine {
355 line_type: DiffLineType::Added,
356 content: new_lines[new_idx].to_string(),
357 line_number_old: None,
358 line_number_new: Some(new_idx + 1),
359 });
360 additions += 1;
361 new_idx += 1;
362 }
363 }
364
365 let changes = additions + deletions;
366
367 FileDiff {
368 file_path: file_path.to_string(),
369 old_content: old_content.to_string(),
370 new_content: new_content.to_string(),
371 lines,
372 stats: DiffStats {
373 additions,
374 deletions,
375 changes,
376 },
377 }
378 }
379
380 fn find_difference(
381 &self,
382 old_lines: &[&str],
383 new_lines: &[&str],
384 start_old: usize,
385 start_new: usize,
386 ) -> (usize, usize) {
387 let mut old_end = start_old;
388 let mut new_end = start_new;
389
390 while old_end < old_lines.len() && new_end < new_lines.len() {
392 if old_lines[old_end] == new_lines[new_end] {
393 return (old_end, new_end);
394 }
395
396 let mut found = false;
398 for i in 1..=self.context_lines {
399 if old_end + i < old_lines.len() && new_end + i < new_lines.len() {
400 if old_lines[old_end + i] == new_lines[new_end + i] {
401 old_end += i;
402 new_end += i;
403 found = true;
404 break;
405 }
406 }
407 }
408
409 if !found {
410 old_end += 1;
411 new_end += 1;
412 }
413 }
414
415 (old_end, new_end)
416 }
417}
418
419pub struct DiffChatRenderer {
420 diff_renderer: DiffRenderer,
421}
422
423impl DiffChatRenderer {
424 pub fn new(show_line_numbers: bool, context_lines: usize, use_colors: bool) -> Self {
425 Self {
426 diff_renderer: DiffRenderer::new(show_line_numbers, context_lines, use_colors),
427 }
428 }
429
430 pub fn render_file_change(
431 &self,
432 file_path: &Path,
433 old_content: &str,
434 new_content: &str,
435 ) -> String {
436 let diff = self.diff_renderer.generate_diff(
437 old_content,
438 new_content,
439 &file_path.to_string_lossy(),
440 );
441 self.diff_renderer.render_diff(&diff)
442 }
443
444 pub fn render_multiple_changes(&self, changes: Vec<(String, String, String)>) -> String {
445 let mut output = format!("\nMultiple File Changes ({} files)\n", changes.len());
446 output.push_str("═".repeat(60).as_str());
447 output.push_str("\n\n");
448
449 for (file_path, old_content, new_content) in changes {
450 let diff = self
451 .diff_renderer
452 .generate_diff(&old_content, &new_content, &file_path);
453 output.push_str(&self.diff_renderer.render_diff(&diff));
454 }
455
456 output
457 }
458
459 pub fn render_operation_summary(
460 &self,
461 operation: &str,
462 files_affected: usize,
463 success: bool,
464 ) -> String {
465 let status = if success { "[Success]" } else { "[Failure]" };
466 let mut summary = format!("\n{} {}\n", status, operation);
467 summary.push_str(&format!(" Files affected: {}\n", files_affected));
468
469 if success {
470 summary.push_str("Operation completed successfully!\n");
471 } else {
472 summary.push_str(" Operation completed with errors\n");
473 }
474
475 summary
476 }
477}
478
479pub fn generate_unified_diff(old_content: &str, new_content: &str, filename: &str) -> String {
480 let mut diff = format!("--- a/{}\n+++ b/{}\n", filename, filename);
481
482 let old_lines: Vec<&str> = old_content.lines().collect();
483 let new_lines: Vec<&str> = new_content.lines().collect();
484
485 let mut old_idx = 0;
486 let mut new_idx = 0;
487
488 while old_idx < old_lines.len() || new_idx < new_lines.len() {
489 let start_old = old_idx;
491 let start_new = new_idx;
492
493 while old_idx < old_lines.len()
495 && new_idx < new_lines.len()
496 && old_lines[old_idx] == new_lines[new_idx]
497 {
498 old_idx += 1;
499 new_idx += 1;
500 }
501
502 if old_idx == old_lines.len() && new_idx == new_lines.len() {
503 break; }
505
506 let mut end_old = old_idx;
508 let mut end_new = new_idx;
509
510 let mut context_found = false;
512 for i in 0..3 {
513 if end_old + i < old_lines.len() && end_new + i < new_lines.len() {
515 if old_lines[end_old + i] == new_lines[end_new + i] {
516 end_old += i;
517 end_new += i;
518 context_found = true;
519 break;
520 }
521 }
522 }
523
524 if !context_found {
525 end_old = old_lines.len();
526 end_new = new_lines.len();
527 }
528
529 let old_count = end_old - start_old;
531 let new_count = end_new - start_new;
532
533 diff.push_str(&format!(
534 "@@ -{},{} +{},{} @@\n",
535 start_old + 1,
536 old_count,
537 start_new + 1,
538 new_count
539 ));
540
541 for i in (start_old.saturating_sub(3))..start_old {
543 if i < old_lines.len() {
544 diff.push_str(&format!(" {}\n", old_lines[i]));
545 }
546 }
547
548 for i in start_old..end_old {
550 if i < old_lines.len() {
551 diff.push_str(&format!("-{}\n", old_lines[i]));
552 }
553 }
554
555 for i in start_new..end_new {
557 if i < new_lines.len() {
558 diff.push_str(&format!("+{}\n", new_lines[i]));
559 }
560 }
561
562 for i in end_old..(end_old + 3) {
564 if i < old_lines.len() {
565 diff.push_str(&format!(" {}\n", old_lines[i]));
566 }
567 }
568
569 old_idx = end_old;
570 new_idx = end_new;
571 }
572
573 diff
574}