// File replacement functionality for wjfind
use std::fs
use std::io
use std::path
use std::log
use ./config::Config
use ./search::Match
pub struct ReplaceManager {
dry_run: bool,
backup: bool,
}
impl ReplaceManager {
pub fn new(dry_run: bool, backup: bool) -> Self {
ReplaceManager {
dry_run: dry_run,
backup: backup,
}
}
pub fn replace_in_files(
&self,
matches: Vec<Match>,
replacement: &string
) -> Result<ReplaceStats, string> {
let mut stats = ReplaceStats {
files_modified: 0,
total_replacements: 0,
errors: vec![],
}
// Group matches by file
let mut files_to_replace = std::collections::HashMap::new()
for m in matches {
files_to_replace.entry(m.file.clone())
.or_insert(vec![])
.push(m)
}
// Process each file
for (file_path, file_matches) in files_to_replace {
match self.replace_in_file(
file_path.clone(),
file_matches,
replacement.clone()
) {
Ok(count) => {
stats.files_modified += 1
stats.total_replacements += count
}
Err(e) => {
stats.errors.push(ReplaceError {
file: file_path,
error: e,
})
}
}
}
Ok(stats)
}
fn replace_in_file(
&self,
file_path: string,
matches: Vec<Match>,
replacement: string
) -> Result<int, string> {
// Read original file contents
let contents = fs::read_to_string(file_path.clone())?
// Create backup if requested
if self.backup && !self.dry_run {
let backup_path = format!("{}.bak", file_path)
fs::copy(file_path.clone(), backup_path)?
}
// Sort matches by line number (descending) to replace from bottom up
let mut sorted_matches = matches.clone()
sorted_matches.sort_by(|a, b| b.line_number.cmp(&a.line_number))
// Split into lines for replacement
let mut lines: Vec<string> = contents.lines().map(|s| s.to_string()).collect()
let mut replacement_count = 0
// Replace each match
for m in sorted_matches {
let line_idx = (m.line_number - 1) as usize
if line_idx >= lines.len() {
log::warn("Line number out of bounds", {
"file": file_path.clone(),
"line": m.line_number,
})
continue
}
// Replace the match text in the line
let old_line = &lines[line_idx]
let new_line = old_line.replace(&m.match_text, &replacement)
lines[line_idx] = new_line
replacement_count += 1
}
// Write back to file (unless dry run)
if !self.dry_run {
let new_contents = lines.join("\n")
fs::write(file_path.clone(), new_contents)?
log::info("File modified", {
"file": file_path,
"replacements": replacement_count,
})
} else {
log::info("Dry run - would modify file", {
"file": file_path,
"replacements": replacement_count,
})
}
Ok(replacement_count)
}
pub fn preview_replacements(
&self,
matches: Vec<Match>,
replacement: &string
) -> Vec<ReplacementPreview> {
let mut previews = vec![]
for m in matches {
let new_text = m.line_text.replace(&m.match_text, &replacement)
previews.push(ReplacementPreview {
file: m.file,
line_number: m.line_number,
old_line: m.line_text,
new_line: new_text,
match_text: m.match_text,
replacement: replacement.clone(),
})
}
previews
}
pub fn restore_from_backup(file_path: string) -> Result<(), string> {
let backup_path = format!("{}.bak", file_path)
if !fs::exists(backup_path.clone()) {
return Err(format!("Backup file not found: {}", backup_path))
}
fs::copy(backup_path, file_path.clone())?
log::info("File restored from backup", {
"file": file_path,
})
Ok(())
}
pub fn cleanup_backups(directory: string) -> Result<int, string> {
let mut count = 0
for entry in fs::read_dir(directory)? {
let path = entry.path()
if path.ends_with(".bak") {
fs::remove_file(path)?
count += 1
}
}
log::info("Backup files cleaned up", {
"count": count,
})
Ok(count)
}
}
@derive(Debug, Clone)
pub struct ReplaceStats {
pub files_modified: int,
pub total_replacements: int,
pub errors: Vec<ReplaceError>,
}
@derive(Debug, Clone)
pub struct ReplaceError {
pub file: string,
pub error: string,
}
@derive(Debug, Clone)
pub struct ReplacementPreview {
pub file: string,
pub line_number: int,
pub old_line: string,
pub new_line: string,
pub match_text: string,
pub replacement: string,
}