use async_trait::async_trait;
use serde::Deserialize;
use std::path::PathBuf;
use super::sandbox::resolve_workspace_existing_path;
use super::traits::*;
pub struct EditTool {
workspace: PathBuf,
}
impl EditTool {
pub fn new(workspace: PathBuf) -> Self {
Self { workspace }
}
}
#[derive(Deserialize)]
struct EditArgs {
#[serde(alias = "file_path")]
path: String,
#[serde(alias = "oldText")]
old_string: String,
#[serde(alias = "newText")]
new_string: String,
}
#[async_trait]
impl Tool for EditTool {
fn name(&self) -> &str {
"Edit"
}
fn spec(&self) -> ToolSpec {
ToolSpec {
name: "Edit".to_string(),
description: "Edit a file by replacing exact text. The old_string must match exactly (including whitespace). Use this for precise, surgical edits.".to_string(),
parameters: serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to edit (relative to workspace or absolute)"
},
"old_string": {
"type": "string",
"description": "Exact text to find and replace (must match exactly)"
},
"new_string": {
"type": "string",
"description": "New text to replace the old text with"
}
},
"required": ["path", "old_string", "new_string"]
}),
}
}
async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
let args: EditArgs = serde_json::from_str(arguments)?;
let full_path = match resolve_workspace_existing_path(&self.workspace, &args.path) {
Ok(path) => path,
Err(err) => return Ok(ToolResult::error(err.to_string())),
};
let content = match tokio::fs::read_to_string(&full_path).await {
Ok(c) => c,
Err(e) => {
return Ok(ToolResult::error(format!(
"Cannot read file '{}': {}",
args.path, e
)))
}
};
if !content.contains(&args.old_string) {
return Ok(ToolResult::error(format!(
"Could not find the exact text in {}. The old_string must match exactly including all whitespace and newlines.",
args.path
)));
}
let new_content = content.replacen(&args.old_string, &args.new_string, 1);
match tokio::fs::write(&full_path, &new_content).await {
Ok(_) => Ok(ToolResult::success(format!(
"Successfully replaced text in {}",
args.path
))),
Err(e) => Ok(ToolResult::error(format!("Failed to write: {}", e))),
}
}
}