Skip to main content

matrixcode_core/tools/
edit.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use serde_json::{Value, json};
4
5use super::{Tool, ToolDefinition};
6use crate::approval::RiskLevel;
7
8/// Maximum file size for safe editing (1MB)
9const MAX_EDIT_FILE_SIZE: u64 = 1_000_000;
10
11pub struct EditTool;
12
13#[async_trait]
14impl Tool for EditTool {
15    fn definition(&self) -> ToolDefinition {
16        ToolDefinition {
17            name: "edit".to_string(),
18            description: "在文件中查找精确匹配的字符串并替换为新内容。
19
20【重要】编辑前必须先读取:
21- 你必须在对话中至少用 read 工具读一次该文件
22- 如果尝试编辑前没读文件,此工具会报错
23- 确保了解文件当前状态和上下文后再修改
24
25适用场景:
26- 单处代码修改(改一个函数名)
27- 精确替换(必须唯一匹配)
28- 小范围改动(<10行)
29
30不适用场景:
31- ❌ 同一文件多处修改 → 用 multi_edit(批量替换)
32- ❌ 大范围重构 → 先 enter_plan_mode 规划
33- ❌ 创建新文件 → 用 write
34
35优先级:[高] 小改动首选,精确且安全"
36                .to_string(),
37            parameters: json!({
38                "type": "object",
39                "properties": {
40                    "path": {
41                        "type": "string",
42                        "description": "要编辑的文件路径"
43                    },
44                    "old_string": {
45                        "type": "string",
46                        "description": "要查找并替换的原始字符串(必须精确匹配)"
47                    },
48                    "new_string": {
49                        "type": "string",
50                        "description": "替换后的新字符串"
51                    }
52                },
53                "required": ["path", "old_string", "new_string"]
54            }),
55            ..Default::default()
56        }
57    }
58
59    async fn execute(&self, params: Value) -> Result<String> {
60        let path = params["path"]
61            .as_str()
62            .ok_or_else(|| anyhow::anyhow!("missing 'path'"))?;
63        let old_string = params["old_string"]
64            .as_str()
65            .ok_or_else(|| anyhow::anyhow!("missing 'old_string'"))?;
66        let new_string = params["new_string"]
67            .as_str()
68            .ok_or_else(|| anyhow::anyhow!("missing 'new_string'"))?;
69
70        // Check file size first
71        let metadata = tokio::fs::metadata(path).await?;
72        let file_size = metadata.len();
73
74        if file_size > MAX_EDIT_FILE_SIZE {
75            return Ok(format!(
76                "⚠️ File is too large ({:.1}MB) for safe editing.\n\
77                 Large file edits may cause memory issues.\n\
78                 Consider using other methods:\n\
79                 - Use `bash` with sed/awk for large files\n\
80                 - Split the file into smaller sections first",
81                file_size as f64 / 1_000_000.0
82            ));
83        }
84
85        let content = tokio::fs::read_to_string(path).await?;
86
87        let count = content.matches(old_string).count();
88        if count == 0 {
89            anyhow::bail!("old_string not found in {}", path);
90        }
91        if count > 1 {
92            anyhow::bail!(
93                "old_string found {} times in {} — must be unique",
94                count,
95                path
96            );
97        }
98
99        let new_content = content.replacen(old_string, new_string, 1);
100        tokio::fs::write(path, &new_content).await?;
101
102        // Return diff-style output
103        let old_lines: Vec<&str> = old_string.lines().collect();
104        let new_lines: Vec<&str> = new_string.lines().collect();
105        let mut diff = format!("Successfully edited {}\n", path);
106        for line in &old_lines {
107            diff.push_str(&format!("- {}\n", line));
108        }
109        for line in &new_lines {
110            diff.push_str(&format!("+ {}\n", line));
111        }
112        Ok(diff)
113    }
114
115    fn risk_level(&self) -> RiskLevel {
116        RiskLevel::Mutating
117    }
118}