vtcode_core/ui/
diff_renderer.rs

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        // Simple diff algorithm - can be enhanced with more sophisticated diffing
171        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                    // Same line - context
178                    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                    // Lines differ - find the difference
188                    let (old_end, new_end) =
189                        self.find_difference(&old_lines, &new_lines, old_idx, new_idx);
190
191                    // Add removed lines
192                    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                    // Add added lines
203                    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                // Remaining old lines are deletions
218                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                // Remaining new lines are additions
228                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        // Look for the next matching line
265        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            // Check if we can find a match within context window
271            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        // Find the next difference
364        let start_old = old_idx;
365        let start_new = new_idx;
366
367        // Skip matching lines
368        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; // No more differences
378        }
379
380        // Find the end of the difference
381        let mut end_old = old_idx;
382        let mut end_new = new_idx;
383
384        // Look for next matching context
385        let mut context_found = false;
386        for i in 0..3 {
387            // Look ahead 3 lines for context
388            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        // Generate hunk
404        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        // Add context before
416        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        // Add removed lines
423        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        // Add added lines
430        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        // Add context after
437        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}