1use crate::tool::diff::build_unified_line_diff;
2use crate::tool::fs::resolve_workspace_target;
3use crate::tool::{Tool, ToolResult, ToolSchema, parse_tool_args};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use serde_json::{Value, json};
7use std::path::PathBuf;
8
9pub struct EditTool {
10 workspace_root: PathBuf,
11}
12
13impl EditTool {
14 pub fn new(workspace_root: PathBuf) -> Self {
15 Self { workspace_root }
16 }
17}
18
19#[derive(Debug, Deserialize)]
20struct EditArgs {
21 path: String,
22 old_string: String,
23 new_string: String,
24 #[serde(default)]
25 replace_all: bool,
26}
27
28#[derive(Debug, Serialize)]
29struct EditSummary {
30 added_lines: usize,
31 removed_lines: usize,
32}
33
34#[derive(Debug, Serialize)]
35struct EditOutput {
36 path: String,
37 applied: bool,
38 summary: EditSummary,
39 diff: String,
40}
41
42#[async_trait]
43impl Tool for EditTool {
44 fn schema(&self) -> ToolSchema {
45 ToolSchema {
46 name: "edit".to_string(),
47 description: "Edit a file by replacing an exact string".to_string(),
48 capability: Some("edit".to_string()),
49 mutating: Some(true),
50 parameters: json!({
51 "type": "object",
52 "properties": {
53 "path": {"type": "string"},
54 "old_string": {"type": "string"},
55 "new_string": {"type": "string"},
56 "replace_all": {"type": "boolean"}
57 },
58 "required": ["path", "old_string", "new_string"]
59 }),
60 }
61 }
62
63 async fn execute(&self, args: Value) -> ToolResult {
64 let parsed: EditArgs = match parse_tool_args(args, "edit") {
65 Ok(value) => value,
66 Err(err) => return err,
67 };
68
69 if parsed.old_string.is_empty() {
70 return ToolResult::error("old_string must not be empty");
71 }
72
73 let input_path = PathBuf::from(&parsed.path);
74 let target = match resolve_workspace_target(&self.workspace_root, &input_path) {
75 Ok(path) => path,
76 Err(err) => return ToolResult::error(err),
77 };
78
79 let before = match std::fs::read_to_string(&target) {
80 Ok(content) => content,
81 Err(err) => return ToolResult::error(format!("failed to read file: {err}")),
82 };
83
84 let matches = before.matches(&parsed.old_string).count();
85 if matches == 0 {
86 return ToolResult::error("old_string not found in file");
87 }
88 if !parsed.replace_all && matches > 1 {
89 return ToolResult::error(
90 "old_string is not unique; set replace_all=true to replace all matches",
91 );
92 }
93
94 let after = if parsed.replace_all {
95 before.replace(&parsed.old_string, &parsed.new_string)
96 } else {
97 before.replacen(&parsed.old_string, &parsed.new_string, 1)
98 };
99
100 let applied = before != after;
101
102 if applied && let Err(err) = std::fs::write(&target, &after) {
103 return ToolResult::error(format!("failed to write file: {err}"));
104 }
105
106 let diff = build_unified_line_diff(&before, &after, &parsed.path);
107
108 let output = EditOutput {
109 path: parsed.path,
110 applied,
111 summary: EditSummary {
112 added_lines: diff.added_lines,
113 removed_lines: diff.removed_lines,
114 },
115 diff: diff.unified,
116 };
117
118 ToolResult::ok_json_serializable("ok", &output)
119 }
120}