Skip to main content

morph_cli/core/diff/
renderer.rs

1use colored::Colorize;
2use std::path::Path;
3
4use super::preview::{
5    ChangedFile, DiffHunk, DiffLine, FilePreview, LineType, PreviewConfig, TransformationReport,
6};
7
8#[allow(dead_code)]
9const CONTEXT_LINES: usize = 3;
10
11pub struct DiffRenderer {
12    config: PreviewConfig,
13}
14
15impl DiffRenderer {
16    pub fn new(config: PreviewConfig) -> Self {
17        Self { config }
18    }
19
20    pub fn render_file_preview(&self, preview: &FilePreview) {
21        println!();
22        self.render_file_header(&preview.path);
23        println!();
24
25        if preview.is_binary {
26            self.render_binary_notice();
27            return;
28        }
29
30        if preview.was_truncated {
31            self.render_truncation_notice(preview.line_count);
32        }
33
34        if self.config.summary_only {
35            return;
36        }
37
38        self.render_hunks(&preview.hunks);
39    }
40
41    fn render_file_header(&self, path: &Path) {
42        let display = path.display().to_string();
43        println!("{} {}", "diff".bold().cyan(), display.bold());
44    }
45
46    fn render_binary_notice(&self) {
47        println!("  {} binary file - skipped", "skip".bold().dimmed());
48    }
49
50    fn render_truncation_notice(&self, line_count: usize) {
51        let max_display = self.config.max_lines * 2;
52        println!(
53            "  {} output truncated from {} lines",
54            "note".bold().yellow(),
55            line_count
56        );
57        println!(
58            "  {} use --max-preview-lines={} to adjust",
59            "hint".bold().dimmed(),
60            max_display.saturating_add(100)
61        );
62    }
63
64    fn render_hunks(&self, hunks: &[DiffHunk]) {
65        for hunk in hunks {
66            self.render_hunk(hunk);
67        }
68    }
69
70    fn render_hunk(&self, hunk: &DiffHunk) {
71        println!(
72            "@@ -{},{} +{},{} @@",
73            hunk.old_start, hunk.old_count, hunk.new_start, hunk.new_count
74        );
75
76        let visible_lines: Vec<&DiffLine> = hunk.lines.iter().take(self.config.max_lines).collect();
77        let mut has_more = false;
78
79        if self.config.max_lines > 0 && hunk.lines.len() > self.config.max_lines {
80            has_more = true;
81        }
82
83        for line in visible_lines {
84            self.render_line(line);
85        }
86
87        if has_more {
88            println!(
89                "  {} {} more lines",
90                "note".bold().yellow(),
91                hunk.lines.len() - self.config.max_lines
92            );
93        }
94    }
95
96    fn render_line(&self, line: &DiffLine) {
97        let content = &line.content;
98        match line.line_type {
99            LineType::Addition => {
100                println!("{}", content.green());
101            }
102            LineType::Deletion => {
103                println!("{}", content.red());
104            }
105            LineType::Context => {
106                println!("{}", content.normal());
107            }
108            LineType::Header => {
109                println!("{}", content.cyan().bold());
110            }
111        }
112    }
113
114    pub fn render_changed_file(&self, file: &ChangedFile) {
115        if let Some(preview) = &file.preview {
116            self.render_file_preview(preview);
117        } else {
118            println!();
119            println!("{} {}", "changed".bold().green(), file.path.display());
120        }
121    }
122
123    #[allow(dead_code)]
124    pub fn render_changed_list(&self, report: &TransformationReport) {
125        for file in &report.changed_files {
126            if self.config.verbose || self.config.summary_only {
127                println!(
128                    "{} {} (+{} -{})",
129                    terminal::success_prefix(),
130                    file.path.display(),
131                    file.lines_added,
132                    file.lines_removed
133                );
134            }
135        }
136    }
137
138    pub fn render_skipped_file(&self, path: &Path, reason: &str) {
139        println!(
140            "  {} {} ({})",
141            "skip".bold().dimmed(),
142            path.display(),
143            reason
144        );
145    }
146
147    pub fn render_report(&self, report: &TransformationReport) {
148        println!();
149        println!("{}", "=".repeat(60).cyan());
150        println!("{}", terminal::label("Transformation Report"));
151        println!("{}", "=".repeat(60).cyan());
152
153        if !report.changed_files.is_empty() {
154            println!();
155            println!("{}", "Changed Files:".bold().green());
156            for file in &report.changed_files {
157                self.render_changed_file(file);
158            }
159        }
160
161        if !report.skipped_files.is_empty() {
162            println!();
163            println!("{}", "⚡ Skipped Files (Grouped by Reason):".bold().yellow());
164            let mut grouped_skipped: std::collections::HashMap<String, Vec<&super::preview::SkippedFile>> = std::collections::HashMap::new();
165            for skipped in &report.skipped_files {
166                grouped_skipped.entry(skipped.reason.to_string()).or_default().push(skipped);
167            }
168            for (reason, files) in grouped_skipped {
169                println!("  └─ {} ({} files):", reason.bold().cyan(), files.len());
170                for f in files.iter().take(5) {
171                    println!("     • {}", f.path.display());
172                }
173                if files.len() > 5 {
174                    println!("     • ... and {} more files", files.len() - 5);
175                }
176            }
177        }
178
179        println!();
180        println!("{}", terminal::label("Statistics"));
181        println!("  total changed: {}", report.total_changed());
182        println!("  lines added:   {}", report.total_lines_added());
183        println!("  lines removed: {}", report.total_lines_removed());
184        println!("  files skipped:  {}", report.total_files_skipped());
185        println!("  execution:     {}ms", report.execution_time_ms);
186    }
187}
188
189mod terminal {
190    use colored::Colorize;
191
192    pub fn label(text: &str) -> colored::ColoredString {
193        text.bold().cyan()
194    }
195
196    #[allow(dead_code)]
197    pub fn success_prefix() -> colored::ColoredString {
198        "done".bold().green()
199    }
200}