// tools/edit.rs — Search-and-replace file editing // // The edit tool performs exact string replacement in files. This is the // same pattern used by Claude Code and aider — it's more reliable than // line-number-based editing because the model specifies what it sees, // not where it thinks it is. // // Supports replace_all for bulk renaming (e.g. variable renames). use anyhow::{Context, Result}; use serde::Deserialize; use serde_json::json; use super::ToolDef; #[derive(Deserialize)] struct Args { file_path: String, old_string: String, new_string: String, #[serde(default)] replace_all: bool, } pub fn definition() -> ToolDef { ToolDef::new( "edit_file", "Perform exact string replacement in a file. The old_string must appear \ exactly once in the file (unless replace_all is true). Use read_file first \ to see the current contents.", json!({ "type": "object", "properties": { "file_path": { "type": "string", "description": "Absolute path to the file to edit" }, "old_string": { "type": "string", "description": "The exact text to find and replace" }, "new_string": { "type": "string", "description": "The replacement text" }, "replace_all": { "type": "boolean", "description": "Replace all occurrences (default false)" } }, "required": ["file_path", "old_string", "new_string"] }), ) } pub fn edit_file(args: &serde_json::Value) -> Result { let a: Args = serde_json::from_value(args.clone()) .context("invalid edit_file arguments")?; if a.old_string == a.new_string { anyhow::bail!("old_string and new_string are identical"); } let content = std::fs::read_to_string(&a.file_path) .with_context(|| format!("Failed to read {}", a.file_path))?; let count = content.matches(&*a.old_string).count(); if count == 0 { anyhow::bail!("old_string not found in {}", a.file_path); } if a.replace_all { let new_content = content.replace(&*a.old_string, &a.new_string); std::fs::write(&a.file_path, &new_content) .with_context(|| format!("Failed to write {}", a.file_path))?; Ok(format!("Replaced {} occurrences in {}", count, a.file_path)) } else { if count > 1 { anyhow::bail!( "old_string appears {} times in {} — use replace_all or provide more context \ to make it unique", count, a.file_path ); } let new_content = content.replacen(&*a.old_string, &a.new_string, 1); std::fs::write(&a.file_path, &new_content) .with_context(|| format!("Failed to write {}", a.file_path))?; Ok(format!("Edited {}", a.file_path)) } }