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
8pub struct EditTool;
9
10#[async_trait]
11impl Tool for EditTool {
12    fn definition(&self) -> ToolDefinition {
13        ToolDefinition {
14            name: "edit".to_string(),
15            description: "在文件中查找精确匹配的字符串并替换为新内容".to_string(),
16            parameters: json!({
17                "type": "object",
18                "properties": {
19                    "path": {
20                        "type": "string",
21                        "description": "要编辑的文件路径"
22                    },
23                    "old_string": {
24                        "type": "string",
25                        "description": "要查找并替换的原始字符串(必须精确匹配)"
26                    },
27                    "new_string": {
28                        "type": "string",
29                        "description": "替换后的新字符串"
30                    }
31                },
32                "required": ["path", "old_string", "new_string"]
33            }),
34        }
35    }
36
37    async fn execute(&self, params: Value) -> Result<String> {
38        // Create spinner immediately at the start to fill the gap before actual operation
39        // let mut spinner = ToolSpinner::new("preparing edit");
40
41        let path = params["path"]
42            .as_str()
43            .ok_or_else(|| anyhow::anyhow!("missing 'path'"))?;
44        let old_string = params["old_string"]
45            .as_str()
46            .ok_or_else(|| anyhow::anyhow!("missing 'old_string'"))?;
47        let new_string = params["new_string"]
48            .as_str()
49            .ok_or_else(|| anyhow::anyhow!("missing 'new_string'"))?;
50
51        // Update spinner message for the actual edit operation
52        // spinner.set_message(&format!("editing {}", path));
53
54        let content = tokio::fs::read_to_string(path).await?;
55
56        let count = content.matches(old_string).count();
57        if count == 0 {
58            // spinner.finish_error("not found");
59            anyhow::bail!("old_string not found in {}", path);
60        }
61        if count > 1 {
62            // spinner.finish_error("multiple matches");
63            anyhow::bail!(
64                "old_string found {} times in {} — must be unique",
65                count,
66                path
67            );
68        }
69
70        let new_content = content.replacen(old_string, new_string, 1);
71        tokio::fs::write(path, &new_content).await?;
72
73        // Return diff-style output
74        let old_lines: Vec<&str> = old_string.lines().collect();
75        let new_lines: Vec<&str> = new_string.lines().collect();
76        let mut diff = format!("Successfully edited {}\n", path);
77        for line in &old_lines {
78            diff.push_str(&format!("- {}\n", line));
79        }
80        for line in &new_lines {
81            diff.push_str(&format!("+ {}\n", line));
82        }
83        // spinner.finish_success("edited");
84        Ok(diff)
85    }
86
87    fn risk_level(&self) -> RiskLevel {
88        RiskLevel::Mutating
89    }
90}