vtcode_core/ui/
diff_renderer.rs

1use console::Style;
2use std::path::Path;
3
4/// Style presets for diff rendering
5pub struct DiffStyles;
6
7impl DiffStyles {
8    /// File header style (bold blue)
9    pub fn file_header() -> Style {
10        Style::new().bold().blue()
11    }
12
13    /// File path style (bold cyan)
14    pub fn file_path() -> Style {
15        Style::new().bold().cyan()
16    }
17
18    /// Stats header style (bold magenta)
19    pub fn stats_header() -> Style {
20        Style::new().bold().magenta()
21    }
22
23    /// Additions count style (bold green)
24    pub fn additions_count() -> Style {
25        Style::new().bold().green()
26    }
27
28    /// Deletions count style (bold red)
29    pub fn deletions_count() -> Style {
30        Style::new().bold().red()
31    }
32
33    /// Changes count style (bold yellow)
34    pub fn changes_count() -> Style {
35        Style::new().bold().yellow()
36    }
37
38    /// Summary header style (bold cyan)
39    pub fn summary_header() -> Style {
40        Style::new().bold().cyan()
41    }
42
43    /// Added line style (green)
44    pub fn added_line() -> Style {
45        Style::new().green()
46    }
47
48    /// Removed line style (red)
49    pub fn removed_line() -> Style {
50        Style::new().red()
51    }
52
53    /// Context line style (dim white)
54    pub fn context_line() -> Style {
55        Style::new().white().dim()
56    }
57
58    /// Header line style (bold blue)
59    pub fn header_line() -> Style {
60        Style::new().bold().blue()
61    }
62
63    /// Apply style to text
64    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        // File header
120        output.push_str(&self.render_header(&diff.file_path, &diff.stats));
121
122        // Render each diff line
123        for line in &diff.lines {
124            output.push_str(&self.render_line(line));
125            output.push('\n');
126        }
127
128        // Footer with summary
129        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        // Simple diff algorithm - can be enhanced with more sophisticated diffing
297        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                    // Same line - context
304                    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                    // Lines differ - find the difference
314                    let (old_end, new_end) =
315                        self.find_difference(&old_lines, &new_lines, old_idx, new_idx);
316
317                    // Add removed lines
318                    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                    // Add added lines
329                    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                // Remaining old lines are deletions
344                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                // Remaining new lines are additions
354                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        // Look for the next matching line
391        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            // Check if we can find a match within context window
397            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        // Find the next difference
490        let start_old = old_idx;
491        let start_new = new_idx;
492
493        // Skip matching lines
494        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; // No more differences
504        }
505
506        // Find the end of the difference
507        let mut end_old = old_idx;
508        let mut end_new = new_idx;
509
510        // Look for next matching context
511        let mut context_found = false;
512        for i in 0..3 {
513            // Look ahead 3 lines for context
514            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        // Generate hunk
530        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        // Add context before
542        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        // Add removed lines
549        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        // Add added lines
556        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        // Add context after
563        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}