deepseek_rust_cli/tools/file/
read_write.rs1use 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}