Skip to main content

deepseek_rust_cli/tools/file/
read_write.rs

1use std::{collections::HashMap, path::Path};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde_json::Value;
6
7use crate::{agent::types::UndoAction, tools, tools::base::Tool};
8
9pub struct ReadFileTool;
10#[async_trait]
11impl Tool for ReadFileTool {
12    fn name(&self) -> &str {
13        "read_local_file"
14    }
15    async fn execute(
16        &self,
17        args: &HashMap<String, Value>,
18        _undo: &mut Vec<UndoAction>,
19        _cwd: Option<&Path>,
20    ) -> Result<String> {
21        let path = args
22            .get("file_path")
23            .and_then(|v| v.as_str())
24            .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
25        let start = args
26            .get("start_line")
27            .and_then(|v| v.as_u64())
28            .map(|v| v as usize);
29        let end = args
30            .get("end_line")
31            .and_then(|v| v.as_u64())
32            .map(|v| v as usize);
33        tools::file_io::read_local_file(path, start, end).await
34    }
35}
36
37pub struct WriteFileTool;
38#[async_trait]
39impl Tool for WriteFileTool {
40    fn name(&self) -> &str {
41        "write_local_file"
42    }
43    async fn execute(
44        &self,
45        args: &HashMap<String, Value>,
46        undo: &mut Vec<UndoAction>,
47        _cwd: Option<&Path>,
48    ) -> Result<String> {
49        let path = args
50            .get("file_path")
51            .and_then(|v| v.as_str())
52            .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
53        let content = args
54            .get("content")
55            .and_then(|v| v.as_str())
56            .ok_or_else(|| anyhow::anyhow!("Missing 'content'"))?;
57        let p = crate::tools::base::validate_path(path)?;
58        let backup = tokio::fs::read(&p).await.ok();
59        undo.push(UndoAction {
60            r#type: "write".to_string(),
61            path: p.to_string_lossy().to_string(),
62            backup,
63        });
64        tools::file_io::write_local_file(p.to_str().unwrap(), content)
65            .await
66            .map(|_| "File written.".to_string())
67    }
68}
69
70pub struct ReplaceTextTool;
71#[async_trait]
72impl Tool for ReplaceTextTool {
73    fn name(&self) -> &str {
74        "replace_text_in_file"
75    }
76    async fn execute(
77        &self,
78        args: &HashMap<String, Value>,
79        undo: &mut Vec<UndoAction>,
80        _cwd: Option<&Path>,
81    ) -> Result<String> {
82        let path = args
83            .get("file_path")
84            .and_then(|v| v.as_str())
85            .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
86        let old = args
87            .get("old_text")
88            .and_then(|v| v.as_str())
89            .ok_or_else(|| anyhow::anyhow!("Missing 'old_text'"))?;
90        let new = args
91            .get("new_text")
92            .and_then(|v| v.as_str())
93            .ok_or_else(|| anyhow::anyhow!("Missing 'new_text'"))?;
94        let p = crate::tools::base::validate_path(path)?;
95        let backup = tokio::fs::read(&p).await.ok();
96        undo.push(UndoAction {
97            r#type: "replace".to_string(),
98            path: p.to_string_lossy().to_string(),
99            backup,
100        });
101        tools::file_io::fuzzy_replace_in_file(p.to_str().unwrap(), old, new).await
102    }
103}
104
105pub struct RegexReplaceTool;
106#[async_trait]
107impl Tool for RegexReplaceTool {
108    fn name(&self) -> &str {
109        "regex_replace_in_file"
110    }
111    async fn execute(
112        &self,
113        args: &HashMap<String, Value>,
114        undo: &mut Vec<UndoAction>,
115        _cwd: Option<&Path>,
116    ) -> Result<String> {
117        let path = args
118            .get("file_path")
119            .and_then(|v| v.as_str())
120            .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
121        let regex_str = args
122            .get("regex")
123            .and_then(|v| v.as_str())
124            .ok_or_else(|| anyhow::anyhow!("Missing 'regex'"))?;
125        let replacement = args
126            .get("replacement")
127            .and_then(|v| v.as_str())
128            .ok_or_else(|| anyhow::anyhow!("Missing 'replacement'"))?;
129
130        let p = crate::tools::base::validate_path(path)?;
131        let re = regex::Regex::new(regex_str)?;
132        let content = tokio::fs::read_to_string(&p).await?;
133
134        let backup = Some(content.as_bytes().to_vec());
135        undo.push(UndoAction {
136            r#type: "replace".to_string(),
137            path: p.to_string_lossy().to_string(),
138            backup,
139        });
140
141        let new_content = re.replace_all(&content, replacement).to_string();
142        tokio::fs::write(&p, new_content).await?;
143        Ok("Regex replacement complete.".to_string())
144    }
145}
146
147pub struct JsonUpdateValueTool;
148#[async_trait]
149impl Tool for JsonUpdateValueTool {
150    fn name(&self) -> &str {
151        "json_update_value"
152    }
153    async fn execute(
154        &self,
155        args: &HashMap<String, Value>,
156        undo: &mut Vec<UndoAction>,
157        _cwd: Option<&Path>,
158    ) -> Result<String> {
159        let path = args
160            .get("file_path")
161            .and_then(|v| v.as_str())
162            .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
163        let key_path = args
164            .get("key_path")
165            .and_then(|v| v.as_str())
166            .ok_or_else(|| anyhow::anyhow!("Missing 'key_path'"))?;
167        let new_value_str = args
168            .get("new_value")
169            .and_then(|v| v.as_str())
170            .ok_or_else(|| anyhow::anyhow!("Missing 'new_value'"))?;
171
172        let p = crate::tools::base::validate_path(path)?;
173        let new_val: serde_json::Value = serde_json::from_str(new_value_str)
174            .unwrap_or_else(|_| serde_json::Value::String(new_value_str.to_string()));
175
176        let raw_content = tokio::fs::read(&p).await?;
177        let mut json_data: serde_json::Value = serde_json::from_slice(&raw_content)?;
178
179        undo.push(UndoAction {
180            r#type: "replace".to_string(),
181            path: p.to_string_lossy().to_string(),
182            backup: Some(raw_content),
183        });
184
185        let mut parts = Vec::new();
186        let mut current_part = String::new();
187        let mut chars = key_path.chars().peekable();
188        while let Some(c) = chars.next() {
189            if c == '\\' && chars.peek() == Some(&'.') {
190                current_part.push('.');
191                chars.next();
192            } else if c == '.' {
193                parts.push(current_part);
194                current_part = String::new();
195            } else {
196                current_part.push(c);
197            }
198        }
199        parts.push(current_part);
200
201        if parts.is_empty() || (parts.len() == 1 && parts[0].is_empty()) {
202            return Err(anyhow::anyhow!("Empty key_path"));
203        }
204
205        let mut current = &mut json_data;
206        for (i, part) in parts.iter().enumerate() {
207            if i == parts.len() - 1 {
208                if let Some(obj) = current.as_object_mut() {
209                    obj.insert(part.clone(), new_val.clone());
210                } else {
211                    return Err(anyhow::anyhow!("Value at path is not a JSON object"));
212                }
213            } else {
214                if !current.is_object() {
215                    *current = serde_json::Value::Object(serde_json::Map::new());
216                }
217                let obj = current.as_object_mut().unwrap();
218                if !obj.contains_key(part) {
219                    obj.insert(
220                        part.clone(),
221                        serde_json::Value::Object(serde_json::Map::new()),
222                    );
223                }
224                current = obj.get_mut(part).unwrap();
225            }
226        }
227
228        let updated_raw = serde_json::to_vec_pretty(&json_data)?;
229        tokio::fs::write(&p, updated_raw).await?;
230        Ok(format!("Successfully updated JSON path '{}'.", key_path))
231    }
232}
233
234pub struct EditFileByLinesTool;
235#[async_trait]
236impl Tool for EditFileByLinesTool {
237    fn name(&self) -> &str {
238        "edit_file_by_lines"
239    }
240    async fn execute(
241        &self,
242        args: &HashMap<String, Value>,
243        undo: &mut Vec<UndoAction>,
244        _cwd: Option<&Path>,
245    ) -> Result<String> {
246        let path = args
247            .get("file_path")
248            .and_then(|v| v.as_str())
249            .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
250        let edits_val = args
251            .get("edits")
252            .ok_or_else(|| anyhow::anyhow!("Missing 'edits'"))?;
253
254        let edits: Vec<tools::file_io::LineEdit> = serde_json::from_value(edits_val.clone())?;
255
256        let p = crate::tools::base::validate_path(path)?;
257        let backup = tokio::fs::read(&p).await.ok();
258        undo.push(UndoAction {
259            r#type: "replace".to_string(),
260            path: p.to_string_lossy().to_string(),
261            backup,
262        });
263
264        tools::file_io::edit_file_by_lines(p.to_str().unwrap(), edits).await
265    }
266}
267
268pub struct ApplyDiffPatchTool;
269#[async_trait]
270impl Tool for ApplyDiffPatchTool {
271    fn name(&self) -> &str {
272        "apply_diff_patch"
273    }
274    async fn execute(
275        &self,
276        args: &HashMap<String, Value>,
277        undo: &mut Vec<UndoAction>,
278        _cwd: Option<&Path>,
279    ) -> Result<String> {
280        let path = args
281            .get("file_path")
282            .and_then(|v| v.as_str())
283            .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
284        let patch_content = args
285            .get("patch_content")
286            .and_then(|v| v.as_str())
287            .ok_or_else(|| anyhow::anyhow!("Missing 'patch_content'"))?;
288
289        let p = crate::tools::base::validate_path(path)?;
290        let backup = tokio::fs::read(&p).await.ok();
291        undo.push(UndoAction {
292            r#type: "replace".to_string(),
293            path: p.to_string_lossy().to_string(),
294            backup,
295        });
296
297        tools::file_io::apply_diff_patch(p.to_str().unwrap(), patch_content).await
298    }
299}