1use std::path::{Path, PathBuf};
2use std::time::Instant;
3
4use anyhow::Result;
5
6use crate::utils::terminal;
7
8#[derive(Debug, Clone)]
9#[allow(dead_code)]
10pub struct FilePreview {
11 pub path: PathBuf,
12 pub original_content: String,
13 pub transformed_content: String,
14 pub hunks: Vec<DiffHunk>,
15 pub is_binary: bool,
16 pub was_truncated: bool,
17 pub line_count: usize,
18}
19
20#[derive(Debug, Clone)]
21pub struct DiffHunk {
22 pub old_start: usize,
23 pub old_count: usize,
24 pub new_start: usize,
25 pub new_count: usize,
26 pub lines: Vec<DiffLine>,
27}
28
29#[derive(Debug, Clone)]
30pub struct DiffLine {
31 pub content: String,
32 pub line_type: LineType,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36#[allow(dead_code)]
37pub enum LineType {
38 Context,
39 Addition,
40 Deletion,
41 Header,
42}
43
44#[derive(Debug, Clone)]
45pub struct TransformationReport {
46 pub changed_files: Vec<ChangedFile>,
47 pub skipped_files: Vec<SkippedFile>,
48 pub execution_time_ms: u64,
49 pub start_time: Instant,
50}
51
52impl TransformationReport {
53 pub fn new() -> Self {
54 Self {
55 changed_files: Vec::new(),
56 skipped_files: Vec::new(),
57 execution_time_ms: 0,
58 start_time: Instant::now(),
59 }
60 }
61
62 pub fn finish(&mut self) {
63 self.execution_time_ms = self.start_time.elapsed().as_millis() as u64;
64 }
65
66 pub fn total_changed(&self) -> usize {
67 self.changed_files.len()
68 }
69
70 pub fn total_lines_added(&self) -> usize {
71 self.changed_files.iter().map(|f| f.lines_added).sum()
72 }
73
74 pub fn total_lines_removed(&self) -> usize {
75 self.changed_files.iter().map(|f| f.lines_removed).sum()
76 }
77
78 pub fn total_files_skipped(&self) -> usize {
79 self.skipped_files.len()
80 }
81}
82
83#[derive(Debug, Clone)]
84pub struct ChangedFile {
85 pub path: PathBuf,
86 pub lines_added: usize,
87 pub lines_removed: usize,
88 pub preview: Option<FilePreview>,
89}
90
91#[derive(Debug, Clone)]
92pub struct SkippedFile {
93 pub path: PathBuf,
94 pub reason: SkipReason,
95}
96
97#[derive(Debug, Clone)]
98#[allow(dead_code)]
99pub enum SkipReason {
100 Binary,
101 Empty,
102 UnsupportedEncoding,
103 ExceedsMaxSize,
104 NoChanges,
105}
106
107impl std::fmt::Display for SkipReason {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 SkipReason::Binary => write!(f, "binary file"),
111 SkipReason::Empty => write!(f, "empty file"),
112 SkipReason::UnsupportedEncoding => write!(f, "unsupported encoding"),
113 SkipReason::ExceedsMaxSize => write!(f, "exceeds max size"),
114 SkipReason::NoChanges => write!(f, "no changes detected"),
115 }
116 }
117}
118
119#[derive(Debug, Clone, Copy)]
120#[allow(dead_code)]
121pub struct PreviewConfig {
122 pub max_lines: usize,
123 pub show_line_numbers: bool,
124 pub summary_only: bool,
125 pub verbose: bool,
126}
127
128impl PreviewConfig {
129 #[allow(dead_code)]
130 pub fn new(max_lines: usize) -> Self {
131 Self {
132 max_lines,
133 show_line_numbers: true,
134 summary_only: false,
135 verbose: false,
136 }
137 }
138}
139
140#[allow(dead_code)]
141pub fn create_preview(
142 path: &Path,
143 original: &str,
144 transformed: &str,
145 config: PreviewConfig,
146) -> Result<FilePreview> {
147 let is_binary = contains_binary_content(original);
148 let mut was_truncated = false;
149
150 let original_lines: Vec<&str> = if config.max_lines > 0 {
151 original.lines().take(config.max_lines * 2).collect()
152 } else {
153 original.lines().collect()
154 };
155
156 let line_count = original_lines.len();
157 if config.max_lines > 0 && line_count > config.max_lines * 2 {
158 was_truncated = true;
159 }
160
161 let hunks = if !is_binary && !original.is_empty() && !transformed.is_empty() {
162 compute_diff_hunks(
163 original_lines,
164 transformed.lines().collect::<Vec<_>>(),
165 &config,
166 )
167 } else {
168 Vec::new()
169 };
170
171 Ok(FilePreview {
172 path: path.to_path_buf(),
173 original_content: original.to_string(),
174 transformed_content: transformed.to_string(),
175 hunks,
176 is_binary,
177 was_truncated,
178 line_count,
179 })
180}
181
182#[allow(dead_code)]
183fn compute_diff_hunks(
184 old_lines: Vec<&str>,
185 new_lines: Vec<&str>,
186 config: &PreviewConfig,
187) -> Vec<DiffHunk> {
188 let mut hunks = Vec::new();
189 let max_lines = config.max_lines.saturating_add(1);
190
191 let old_max = if max_lines > 0 && old_lines.len() > max_lines {
192 max_lines
193 } else {
194 old_lines.len()
195 };
196
197 let new_max = if max_lines > 0 && new_lines.len() > max_lines {
198 max_lines
199 } else {
200 new_lines.len()
201 };
202
203 for i in 0..old_max {
204 if old_max == old_lines.len() && i < new_max && old_lines[i] != new_lines[i] {
205 let hunk = DiffHunk {
206 old_start: i + 1,
207 old_count: 1,
208 new_start: i + 1,
209 new_count: 1,
210 lines: vec![
211 DiffLine {
212 content: format!("- {}", old_lines[i]),
213 line_type: LineType::Deletion,
214 },
215 DiffLine {
216 content: format!("+ {}", new_lines[i]),
217 line_type: LineType::Addition,
218 },
219 ],
220 };
221 hunks.push(hunk);
222 break;
223 }
224 }
225
226 hunks
227}
228
229#[allow(dead_code)]
230fn contains_binary_content(content: &str) -> bool {
231 for byte in content.bytes() {
232 if byte == 0 {
233 return true;
234 }
235 }
236 false
237}
238
239#[allow(dead_code)]
240pub fn is_valid_utf8(content: &str) -> bool {
241 content.is_empty() || std::str::from_utf8(content.as_bytes()).is_ok()
242}
243
244#[allow(dead_code)]
245pub fn summarize_report(report: &TransformationReport) {
246 println!();
247 println!("{}", terminal::label("Transformation Summary"));
248 println!(" changed files: {}", report.total_changed());
249 println!(" lines added: {}", report.total_lines_added());
250 println!(" lines removed: {}", report.total_lines_removed());
251 println!(" skipped files: {}", report.total_files_skipped());
252 println!(" execution time: {}ms", report.execution_time_ms);
253 println!();
254}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq)]
257pub enum ReviewAction {
258 Apply,
259 Skip,
260 Abort,
261}
262
263pub fn prompt_review(
264 path: &Path,
265 original: &str,
266 transformed: &str,
267 renderer: &crate::core::diff::renderer::DiffRenderer,
268) -> Result<ReviewAction> {
269 let preview = create_preview(
270 path,
271 original,
272 transformed,
273 crate::core::diff::preview::PreviewConfig {
274 max_lines: 100,
275 show_line_numbers: true,
276 summary_only: false,
277 verbose: false,
278 },
279 )?;
280
281 renderer.render_file_preview(&preview);
282
283 loop {
284 use std::io::Write;
285 use colored::Colorize;
286 println!();
287 println!(" ┌──────────────────────────────────────────────────────────┐");
288 println!(" │ 🔍 {} {:<39} │", "Reviewing file:".dimmed(), path.display().to_string().bold().cyan());
289 println!(" ├──────────────────────────────────────────────────────────┤");
290 println!(" │ {} [y] {:<39} │", "✔".green(), "Apply and write changes to disk safely".bold().green());
291 println!(" │ {} [n] {:<39} │", "⚡".yellow(), "Skip changes for this file cleanly".bold().yellow());
292 println!(" │ {} [q] {:<39} │", "❌".red(), "Abort the entire execution session safely".bold().red());
293 println!(" └──────────────────────────────────────────────────────────┘");
294 print!("👉 Choose an action (y/n/q): ");
295 std::io::stdout().flush()?;
296 let mut input = String::new();
297 std::io::stdin().read_line(&mut input)?;
298 let ans = input.trim().to_lowercase();
299 match ans.as_str() {
300 "y" | "yes" | "apply" => return Ok(ReviewAction::Apply),
301 "n" | "no" | "skip" => return Ok(ReviewAction::Skip),
302 "q" | "quit" | "abort" => return Ok(ReviewAction::Abort),
303 _ => println!("⚠️ {}", "Invalid choice. Please enter 'y', 'n', or 'q'.".red().bold()),
304 }
305 }
306}