use color_eyre::eyre::{Result, eyre}; use ignore::WalkBuilder; use grep_regex::RegexMatcher; use grep_searcher::{sinks::UTF8, SearcherBuilder}; use globset::Glob; use std::path::Path; pub fn read_file(path: &str) -> Result { Ok(std::fs::read_to_string(path)?) } pub fn write_file(path: &str, content: &str) -> Result<()> { // Create parent directories if they don't exist if let Some(parent) = Path::new(path).parent() { std::fs::create_dir_all(parent)?; } std::fs::write(path, content)?; Ok(()) } pub fn edit_file(path: &str, old_string: &str, new_string: &str) -> Result<()> { // Read the current file content let content = std::fs::read_to_string(path)?; // Find all occurrences of old_string let matches: Vec<_> = content.match_indices(old_string).collect(); match matches.len() { 0 => Err(eyre!("String to replace not found in file: '{}'", old_string)), 1 => { // Exactly one match - safe to replace let new_content = content.replace(old_string, new_string); // Create a backup before modifying let backup_path = format!("{}.backup", path); std::fs::write(&backup_path, &content)?; // Write the new content match std::fs::write(path, new_content) { Ok(_) => { // Success - remove backup let _ = std::fs::remove_file(&backup_path); Ok(()) } Err(e) => { // Failed to write - restore from backup let _ = std::fs::rename(&backup_path, path); Err(e.into()) } } } n => Err(eyre!( "Ambiguous replacement: found {} occurrences of '{}' in file. Please make the old_string unique.", n, old_string )), } } pub fn glob_list(pattern: &str) -> Result> { let glob = Glob::new(pattern)?.compile_matcher(); // Extract the literal prefix to determine the root directory // Find the position of the first glob metacharacter let first_glob = pattern .find(|c| matches!(c, '*' | '?' | '[' | '{')) .unwrap_or(pattern.len()); // Find the last directory separator before the first glob metacharacter let root = if first_glob > 0 { let prefix = &pattern[..first_glob]; prefix.rfind('/').map(|pos| &prefix[..pos]).unwrap_or(".") } else { "." }; let mut out = Vec::new(); for result in WalkBuilder::new(root) .standard_filters(true) .git_ignore(true) .git_global(false) .git_exclude(false) .require_git(false) .build() { let entity = result?; if entity.file_type().map(|filetype| filetype.is_file()).unwrap_or(false) { if let Some(path) = entity.path().to_str() { // Match against the glob pattern if glob.is_match(path) { out.push(path.to_string()); } } } } Ok(out) } pub fn grep(root: &str, pattern: &str) -> Result> { let matcher = RegexMatcher::new_line_matcher(pattern)?; let mut searcher = SearcherBuilder::new().line_number(true).build(); let mut results = Vec::new(); for result in WalkBuilder::new(root) .standard_filters(true) .git_ignore(true) .git_global(false) .git_exclude(false) .require_git(false) .build() { let entity = result?; if !entity.file_type().map(|filetype| filetype.is_file()).unwrap_or(false) { continue; } let path = entity.path().to_path_buf(); let mut line_hits: Vec<(usize, String)> = Vec::new(); let sink = UTF8(|line_number, line| { line_hits.push((line_number as usize, line.to_string())); Ok(true) }); let _ = searcher.search_path(&matcher, &path, sink); if !line_hits.is_empty() { let p = path.to_string_lossy().to_string(); for (line_number, text) in line_hits { results.push((p.clone(), line_number, text)); } } } Ok(results) }