use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::Result;
use crate::utils::terminal;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct FilePreview {
pub path: PathBuf,
pub original_content: String,
pub transformed_content: String,
pub hunks: Vec<DiffHunk>,
pub is_binary: bool,
pub was_truncated: bool,
pub line_count: usize,
}
#[derive(Debug, Clone)]
pub struct DiffHunk {
pub old_start: usize,
pub old_count: usize,
pub new_start: usize,
pub new_count: usize,
pub lines: Vec<DiffLine>,
}
#[derive(Debug, Clone)]
pub struct DiffLine {
pub content: String,
pub line_type: LineType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum LineType {
Context,
Addition,
Deletion,
Header,
}
#[derive(Debug, Clone)]
pub struct TransformationReport {
pub changed_files: Vec<ChangedFile>,
pub skipped_files: Vec<SkippedFile>,
pub execution_time_ms: u64,
pub start_time: Instant,
}
impl TransformationReport {
pub fn new() -> Self {
Self {
changed_files: Vec::new(),
skipped_files: Vec::new(),
execution_time_ms: 0,
start_time: Instant::now(),
}
}
pub fn finish(&mut self) {
self.execution_time_ms = self.start_time.elapsed().as_millis() as u64;
}
pub fn total_changed(&self) -> usize {
self.changed_files.len()
}
pub fn total_lines_added(&self) -> usize {
self.changed_files.iter().map(|f| f.lines_added).sum()
}
pub fn total_lines_removed(&self) -> usize {
self.changed_files.iter().map(|f| f.lines_removed).sum()
}
pub fn total_files_skipped(&self) -> usize {
self.skipped_files.len()
}
}
#[derive(Debug, Clone)]
pub struct ChangedFile {
pub path: PathBuf,
pub lines_added: usize,
pub lines_removed: usize,
pub preview: Option<FilePreview>,
}
#[derive(Debug, Clone)]
pub struct SkippedFile {
pub path: PathBuf,
pub reason: SkipReason,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum SkipReason {
Binary,
Empty,
UnsupportedEncoding,
ExceedsMaxSize,
NoChanges,
}
impl std::fmt::Display for SkipReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SkipReason::Binary => write!(f, "binary file"),
SkipReason::Empty => write!(f, "empty file"),
SkipReason::UnsupportedEncoding => write!(f, "unsupported encoding"),
SkipReason::ExceedsMaxSize => write!(f, "exceeds max size"),
SkipReason::NoChanges => write!(f, "no changes detected"),
}
}
}
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub struct PreviewConfig {
pub max_lines: usize,
pub show_line_numbers: bool,
pub summary_only: bool,
pub verbose: bool,
}
impl PreviewConfig {
#[allow(dead_code)]
pub fn new(max_lines: usize) -> Self {
Self {
max_lines,
show_line_numbers: true,
summary_only: false,
verbose: false,
}
}
}
#[allow(dead_code)]
pub fn create_preview(
path: &Path,
original: &str,
transformed: &str,
config: PreviewConfig,
) -> Result<FilePreview> {
let is_binary = contains_binary_content(original);
let mut was_truncated = false;
let original_lines: Vec<&str> = if config.max_lines > 0 {
original.lines().take(config.max_lines * 2).collect()
} else {
original.lines().collect()
};
let line_count = original_lines.len();
if config.max_lines > 0 && line_count > config.max_lines * 2 {
was_truncated = true;
}
let hunks = if !is_binary && !original.is_empty() && !transformed.is_empty() {
compute_diff_hunks(
original_lines,
transformed.lines().collect::<Vec<_>>(),
&config,
)
} else {
Vec::new()
};
Ok(FilePreview {
path: path.to_path_buf(),
original_content: original.to_string(),
transformed_content: transformed.to_string(),
hunks,
is_binary,
was_truncated,
line_count,
})
}
#[allow(dead_code)]
fn compute_diff_hunks(
old_lines: Vec<&str>,
new_lines: Vec<&str>,
config: &PreviewConfig,
) -> Vec<DiffHunk> {
let mut hunks = Vec::new();
let max_lines = config.max_lines.saturating_add(1);
let old_max = if max_lines > 0 && old_lines.len() > max_lines {
max_lines
} else {
old_lines.len()
};
let new_max = if max_lines > 0 && new_lines.len() > max_lines {
max_lines
} else {
new_lines.len()
};
for i in 0..old_max {
if old_max == old_lines.len() && i < new_max && old_lines[i] != new_lines[i] {
let hunk = DiffHunk {
old_start: i + 1,
old_count: 1,
new_start: i + 1,
new_count: 1,
lines: vec![
DiffLine {
content: format!("- {}", old_lines[i]),
line_type: LineType::Deletion,
},
DiffLine {
content: format!("+ {}", new_lines[i]),
line_type: LineType::Addition,
},
],
};
hunks.push(hunk);
break;
}
}
hunks
}
#[allow(dead_code)]
fn contains_binary_content(content: &str) -> bool {
for byte in content.bytes() {
if byte == 0 {
return true;
}
}
false
}
#[allow(dead_code)]
pub fn is_valid_utf8(content: &str) -> bool {
content.is_empty() || std::str::from_utf8(content.as_bytes()).is_ok()
}
#[allow(dead_code)]
pub fn summarize_report(report: &TransformationReport) {
println!();
println!("{}", terminal::label("Transformation Summary"));
println!(" changed files: {}", report.total_changed());
println!(" lines added: {}", report.total_lines_added());
println!(" lines removed: {}", report.total_lines_removed());
println!(" skipped files: {}", report.total_files_skipped());
println!(" execution time: {}ms", report.execution_time_ms);
println!();
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReviewAction {
Apply,
Skip,
Abort,
}
pub fn prompt_review(
path: &Path,
original: &str,
transformed: &str,
renderer: &crate::core::diff::renderer::DiffRenderer,
) -> Result<ReviewAction> {
let preview = create_preview(
path,
original,
transformed,
crate::core::diff::preview::PreviewConfig {
max_lines: 100,
show_line_numbers: true,
summary_only: false,
verbose: false,
},
)?;
renderer.render_file_preview(&preview);
loop {
use std::io::Write;
use colored::Colorize;
println!();
println!(" ┌──────────────────────────────────────────────────────────┐");
println!(" │ 🔍 {} {:<39} │", "Reviewing file:".dimmed(), path.display().to_string().bold().cyan());
println!(" ├──────────────────────────────────────────────────────────┤");
println!(" │ {} [y] {:<39} │", "✔".green(), "Apply and write changes to disk safely".bold().green());
println!(" │ {} [n] {:<39} │", "⚡".yellow(), "Skip changes for this file cleanly".bold().yellow());
println!(" │ {} [q] {:<39} │", "❌".red(), "Abort the entire execution session safely".bold().red());
println!(" └──────────────────────────────────────────────────────────┘");
print!("👉 Choose an action (y/n/q): ");
std::io::stdout().flush()?;
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
let ans = input.trim().to_lowercase();
match ans.as_str() {
"y" | "yes" | "apply" => return Ok(ReviewAction::Apply),
"n" | "no" | "skip" => return Ok(ReviewAction::Skip),
"q" | "quit" | "abort" => return Ok(ReviewAction::Abort),
_ => println!("⚠️ {}", "Invalid choice. Please enter 'y', 'n', or 'q'.".red().bold()),
}
}
}