1use async_trait::async_trait;
5use serde::Deserialize;
6use std::path::PathBuf;
7
8use super::sandbox::resolve_workspace_existing_path;
9use super::traits::*;
10
11pub struct EditTool {
12 workspace: PathBuf,
13}
14
15impl EditTool {
16 pub fn new(workspace: PathBuf) -> Self {
17 Self { workspace }
18 }
19}
20
21#[derive(Deserialize)]
22struct EditArgs {
23 #[serde(alias = "file_path")]
25 path: String,
26 #[serde(alias = "oldText")]
28 old_string: String,
29 #[serde(alias = "newText")]
31 new_string: String,
32}
33
34#[async_trait]
35impl Tool for EditTool {
36 fn name(&self) -> &str {
37 "Edit"
38 }
39
40 fn spec(&self) -> ToolSpec {
41 ToolSpec {
42 name: "Edit".to_string(),
43 description: "Edit a file by replacing exact text. The old_string must match exactly (including whitespace). Use this for precise, surgical edits.".to_string(),
44 parameters: serde_json::json!({
45 "type": "object",
46 "properties": {
47 "path": {
48 "type": "string",
49 "description": "Path to the file to edit (relative to workspace or absolute)"
50 },
51 "old_string": {
52 "type": "string",
53 "description": "Exact text to find and replace (must match exactly)"
54 },
55 "new_string": {
56 "type": "string",
57 "description": "New text to replace the old text with"
58 }
59 },
60 "required": ["path", "old_string", "new_string"]
61 }),
62 }
63 }
64
65 async fn execute(&self, arguments: &str) -> anyhow::Result<ToolResult> {
66 let args: EditArgs = serde_json::from_str(arguments)?;
67
68 let full_path = match resolve_workspace_existing_path(&self.workspace, &args.path) {
69 Ok(path) => path,
70 Err(err) => return Ok(ToolResult::error(err.to_string())),
71 };
72
73 let content = match tokio::fs::read_to_string(&full_path).await {
74 Ok(c) => c,
75 Err(e) => {
76 return Ok(ToolResult::error(format!(
77 "Cannot read file '{}': {}",
78 args.path, e
79 )))
80 }
81 };
82
83 if !content.contains(&args.old_string) {
85 return Ok(ToolResult::error(format!(
86 "Could not find the exact text in {}. The old_string must match exactly including all whitespace and newlines.",
87 args.path
88 )));
89 }
90
91 let new_content = content.replacen(&args.old_string, &args.new_string, 1);
92
93 match tokio::fs::write(&full_path, &new_content).await {
95 Ok(_) => Ok(ToolResult::success(format!(
96 "Successfully replaced text in {}",
97 args.path
98 ))),
99 Err(e) => Ok(ToolResult::error(format!("Failed to write: {}", e))),
100 }
101 }
102}