windjammer 0.48.0

A simple language inspired by Go, Ruby, and Elixir that transpiles to Rust - 80% of Rust's power with 20% of the complexity
Documentation
// 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,
}