deepseek_rust_cli/tools/file/
ops.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 DeleteFileTool;
10#[async_trait]
11impl Tool for DeleteFileTool {
12 fn name(&self) -> &str {
13 "delete_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 p = crate::tools::base::validate_path(path)?;
26 let backup = tokio::fs::read(&p).await.ok();
27 undo.push(UndoAction {
28 r#type: "delete".to_string(),
29 path: p.to_string_lossy().to_string(),
30 backup,
31 });
32 tools::file_io::delete_file(p.to_str().unwrap())
33 .await
34 .map(|_| "File deleted.".to_string())
35 }
36}
37
38pub struct RenameFileTool;
39#[async_trait]
40impl Tool for RenameFileTool {
41 fn name(&self) -> &str {
42 "rename_file"
43 }
44 async fn execute(
45 &self,
46 args: &HashMap<String, Value>,
47 undo: &mut Vec<UndoAction>,
48 _cwd: Option<&Path>,
49 ) -> Result<String> {
50 let src = args
51 .get("source_path")
52 .and_then(|v| v.as_str())
53 .ok_or_else(|| anyhow::anyhow!("Missing 'source_path'"))?;
54 let dst = args
55 .get("destination_path")
56 .and_then(|v| v.as_str())
57 .ok_or_else(|| anyhow::anyhow!("Missing 'destination_path'"))?;
58 let src_p = crate::tools::base::validate_path(src)?;
59 let dst_p = crate::tools::base::validate_path(dst)?;
60 undo.push(UndoAction {
61 r#type: "rename".to_string(),
62 path: dst_p.to_string_lossy().to_string(),
63 backup: Some(src_p.to_string_lossy().as_bytes().to_vec()),
64 });
65 tools::file_io::rename_file(src_p.to_str().unwrap(), dst_p.to_str().unwrap())
66 .await
67 .map(|_| "File moved.".to_string())
68 }
69}
70
71pub struct HashFileTool;
72#[async_trait]
73impl Tool for HashFileTool {
74 fn name(&self) -> &str {
75 "hash_file"
76 }
77 async fn execute(
78 &self,
79 args: &HashMap<String, Value>,
80 _undo: &mut Vec<UndoAction>,
81 _cwd: Option<&Path>,
82 ) -> Result<String> {
83 let path = args
84 .get("path")
85 .and_then(|v| v.as_str())
86 .ok_or_else(|| anyhow::anyhow!("Missing 'path'"))?
87 .to_string();
88 let alg = args
89 .get("algorithm")
90 .and_then(|v| v.as_str())
91 .map(|s| s.to_string());
92 tools::file_ops::hash_file(path, alg).await
93 }
94}
95
96pub struct CountLinesTool;
97#[async_trait]
98impl Tool for CountLinesTool {
99 fn name(&self) -> &str {
100 "count_lines"
101 }
102 async fn execute(
103 &self,
104 args: &HashMap<String, Value>,
105 _undo: &mut Vec<UndoAction>,
106 _cwd: Option<&Path>,
107 ) -> Result<String> {
108 let path = args
109 .get("path")
110 .and_then(|v| v.as_str())
111 .ok_or_else(|| anyhow::anyhow!("Missing 'path'"))?
112 .to_string();
113 tools::file_ops::count_lines(path).await
114 }
115}
116
117pub struct SearchFilesTool;
118#[async_trait]
119impl Tool for SearchFilesTool {
120 fn name(&self) -> &str {
121 "search_files"
122 }
123 async fn execute(
124 &self,
125 args: &HashMap<String, Value>,
126 _undo: &mut Vec<UndoAction>,
127 _cwd: Option<&Path>,
128 ) -> Result<String> {
129 let query = args
130 .get("query")
131 .and_then(|v| v.as_str())
132 .ok_or_else(|| anyhow::anyhow!("Missing 'query'"))?;
133 let path = args.get("path").and_then(|v| v.as_str());
134 let glob = args.get("glob").and_then(|v| v.as_str());
135 let max = args
136 .get("max_results")
137 .and_then(|v| v.as_u64())
138 .unwrap_or(50) as usize;
139 tools::file_io::search_files(query, path, glob, max).await
140 }
141}
142
143pub struct ListSymbolsTool;
144#[async_trait]
145impl Tool for ListSymbolsTool {
146 fn name(&self) -> &str {
147 "list_symbols"
148 }
149 async fn execute(
150 &self,
151 args: &HashMap<String, Value>,
152 _undo: &mut Vec<UndoAction>,
153 _cwd: Option<&Path>,
154 ) -> Result<String> {
155 let path_str = args
156 .get("file_path")
157 .and_then(|v| v.as_str())
158 .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
159 let p = crate::tools::base::validate_path(path_str)?;
160 if !p.exists() {
161 return Err(anyhow::anyhow!("File does not exist: {}", path_str));
162 }
163
164 let content = tokio::fs::read_to_string(&p).await?;
165 let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
166
167 let mut symbols = Vec::new();
168
169 match ext {
170 "rs" => {
171 let re_fn =
172 regex::Regex::new(r"(?m)^(?:\s*pub\s+)?(?:async\s+)?fn\s+([a-zA-Z0-9_]+)\s*")?;
173 let re_struct = regex::Regex::new(r"(?m)^(?:\s*pub\s+)?struct\s+([a-zA-Z0-9_]+)")?;
174 let re_enum = regex::Regex::new(r"(?m)^(?:\s*pub\s+)?enum\s+([a-zA-Z0-9_]+)")?;
175 let re_impl =
176 regex::Regex::new(r"(?m)^(?:\s*pub\s+)?impl(?:\s*<.*>)?\s+([a-zA-Z0-9_]+)")?;
177
178 for (line_idx, line) in content.lines().enumerate() {
179 let line_num = line_idx + 1;
180 if let Some(cap) = re_fn.captures(line) {
181 symbols.push(format!("Line {}: fn {}", line_num, &cap[1]));
182 } else if let Some(cap) = re_struct.captures(line) {
183 symbols.push(format!("Line {}: struct {}", line_num, &cap[1]));
184 } else if let Some(cap) = re_enum.captures(line) {
185 symbols.push(format!("Line {}: enum {}", line_num, &cap[1]));
186 } else if let Some(cap) = re_impl.captures(line) {
187 symbols.push(format!("Line {}: impl {}", line_num, &cap[1]));
188 }
189 }
190 }
191 "py" => {
192 let re_def = regex::Regex::new(r"(?m)^\s*(?:async\s+)?def\s+([a-zA-Z0-9_]+)\s*\(")?;
193 let re_class = regex::Regex::new(r"(?m)^\s*class\s+([a-zA-Z0-9_]+)")?;
194
195 for (line_idx, line) in content.lines().enumerate() {
196 let line_num = line_idx + 1;
197 if let Some(cap) = re_def.captures(line) {
198 symbols.push(format!("Line {}: def {}", line_num, &cap[1]));
199 } else if let Some(cap) = re_class.captures(line) {
200 symbols.push(format!("Line {}: class {}", line_num, &cap[1]));
201 }
202 }
203 }
204 "js" | "ts" | "jsx" | "tsx" => {
205 let re_fn = regex::Regex::new(
206 r"(?m)^(?:\s*export\s+)?(?:async\s+)?function\s+([a-zA-Z0-9_]+)\s*",
207 )?;
208 let re_class = regex::Regex::new(r"(?m)^(?:\s*export\s+)?class\s+([a-zA-Z0-9_]+)")?;
209 let re_const_fn = regex::Regex::new(
210 r"(?m)^(?:\s*export\s+)?const\s+([a-zA-Z0-9_]+)\s*=\s*(?:async\s*)?\(.*?\)\s*=>",
211 )?;
212
213 for (line_idx, line) in content.lines().enumerate() {
214 let line_num = line_idx + 1;
215 if let Some(cap) = re_fn.captures(line) {
216 symbols.push(format!("Line {}: function {}", line_num, &cap[1]));
217 } else if let Some(cap) = re_class.captures(line) {
218 symbols.push(format!("Line {}: class {}", line_num, &cap[1]));
219 } else if let Some(cap) = re_const_fn.captures(line) {
220 symbols.push(format!("Line {}: const function {}", line_num, &cap[1]));
221 }
222 }
223 }
224 _ => {
225 let re_gen =
226 regex::Regex::new(r"(?m)(?:fn|def|function|class|struct)\s+([a-zA-Z0-9_]+)")?;
227 for (line_idx, line) in content.lines().enumerate() {
228 let line_num = line_idx + 1;
229 if re_gen.is_match(line) {
230 symbols.push(format!("Line {}: {}", line_num, line.trim()));
231 }
232 }
233 }
234 }
235
236 if symbols.is_empty() {
237 Ok("No symbols found in file.".to_string())
238 } else {
239 Ok(format!(
240 "Symbols found in {}:\n{}",
241 path_str,
242 symbols.join("\n")
243 ))
244 }
245 }
246}
247
248pub struct BulkRenameTool;
249#[async_trait]
250impl Tool for BulkRenameTool {
251 fn name(&self) -> &str {
252 "bulk_rename"
253 }
254 async fn execute(
255 &self,
256 args: &HashMap<String, Value>,
257 _undo: &mut Vec<UndoAction>,
258 _cwd: Option<&Path>,
259 ) -> Result<String> {
260 let path = args
261 .get("path")
262 .and_then(|v| v.as_str())
263 .ok_or_else(|| anyhow::anyhow!("Missing 'path'"))?;
264 let pattern = args
265 .get("pattern")
266 .and_then(|v| v.as_str())
267 .ok_or_else(|| anyhow::anyhow!("Missing 'pattern'"))?;
268 let replacement = args
269 .get("replacement")
270 .and_then(|v| v.as_str())
271 .ok_or_else(|| anyhow::anyhow!("Missing 'replacement'"))?;
272
273 tools::file_io::bulk_rename(path, pattern, replacement).await
274 }
275}
276
277pub struct CopyFileTool;
278#[async_trait]
279impl Tool for CopyFileTool {
280 fn name(&self) -> &str {
281 "copy_file"
282 }
283 async fn execute(
284 &self,
285 args: &HashMap<String, Value>,
286 undo: &mut Vec<UndoAction>,
287 _cwd: Option<&Path>,
288 ) -> Result<String> {
289 let src = args
290 .get("source_path")
291 .and_then(|v| v.as_str())
292 .ok_or_else(|| anyhow::anyhow!("Missing 'source_path'"))?;
293 let dst = args
294 .get("destination_path")
295 .and_then(|v| v.as_str())
296 .ok_or_else(|| anyhow::anyhow!("Missing 'destination_path'"))?;
297
298 let src_p = crate::tools::base::validate_path(src)?;
299 let dst_p = crate::tools::base::validate_path(dst)?;
300
301 let backup = tokio::fs::read(&dst_p).await.ok();
303 undo.push(UndoAction {
304 r#type: "write".to_string(),
305 path: dst_p.to_string_lossy().to_string(),
306 backup,
307 });
308
309 tools::file_io::copy_local_file(src_p.to_str().unwrap(), dst_p.to_str().unwrap()).await?;
310 Ok(format!("File copied from {} to {}.", src, dst))
311 }
312}
313
314pub struct CopyDirectoryTool;
315#[async_trait]
316impl Tool for CopyDirectoryTool {
317 fn name(&self) -> &str {
318 "copy_directory"
319 }
320 async fn execute(
321 &self,
322 args: &HashMap<String, Value>,
323 _undo: &mut Vec<UndoAction>,
324 _cwd: Option<&Path>,
325 ) -> Result<String> {
326 let src = args
327 .get("source_path")
328 .and_then(|v| v.as_str())
329 .ok_or_else(|| anyhow::anyhow!("Missing 'source_path'"))?;
330 let dst = args
331 .get("destination_path")
332 .and_then(|v| v.as_str())
333 .ok_or_else(|| anyhow::anyhow!("Missing 'destination_path'"))?;
334
335 tools::file_io::copy_directory(src, dst).await?;
336 Ok(format!("Directory copied from {} to {}.", src, dst))
337 }
338}
339
340pub struct CreateDirectoryTool;
341#[async_trait]
342impl Tool for CreateDirectoryTool {
343 fn name(&self) -> &str {
344 "create_directory"
345 }
346 async fn execute(
347 &self,
348 args: &HashMap<String, Value>,
349 _undo: &mut Vec<UndoAction>,
350 _cwd: Option<&Path>,
351 ) -> Result<String> {
352 let path = args
353 .get("directory_path")
354 .and_then(|v| v.as_str())
355 .ok_or_else(|| anyhow::anyhow!("Missing 'directory_path'"))?;
356
357 tools::file_io::create_directory(path).await?;
358 Ok(format!("Directory created: {}", path))
359 }
360}
361
362pub struct FileExistsTool;
363#[async_trait]
364impl Tool for FileExistsTool {
365 fn name(&self) -> &str {
366 "file_exists"
367 }
368 async fn execute(
369 &self,
370 args: &HashMap<String, Value>,
371 _undo: &mut Vec<UndoAction>,
372 _cwd: Option<&Path>,
373 ) -> Result<String> {
374 let path = args
375 .get("file_path")
376 .and_then(|v| v.as_str())
377 .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
378
379 let exists = tools::file_io::file_exists(path).await?;
380 Ok(exists.to_string())
381 }
382}
383
384pub struct GetFileInfoTool;
385#[async_trait]
386impl Tool for GetFileInfoTool {
387 fn name(&self) -> &str {
388 "get_file_info"
389 }
390 async fn execute(
391 &self,
392 args: &HashMap<String, Value>,
393 _undo: &mut Vec<UndoAction>,
394 _cwd: Option<&Path>,
395 ) -> Result<String> {
396 let path = args
397 .get("file_path")
398 .and_then(|v| v.as_str())
399 .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
400
401 tools::file_io::get_file_info(path).await
402 }
403}
404
405pub struct ViewSymbolContentsTool;
406#[async_trait]
407impl Tool for ViewSymbolContentsTool {
408 fn name(&self) -> &str {
409 "view_symbol_contents"
410 }
411 async fn execute(
412 &self,
413 args: &HashMap<String, Value>,
414 _undo: &mut Vec<UndoAction>,
415 _cwd: Option<&Path>,
416 ) -> Result<String> {
417 let path = args
418 .get("file_path")
419 .and_then(|v| v.as_str())
420 .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
421 let symbol_name = args
422 .get("symbol_name")
423 .and_then(|v| v.as_str())
424 .ok_or_else(|| anyhow::anyhow!("Missing 'symbol_name'"))?;
425
426 tools::file_ops::view_symbol_contents(path, symbol_name).await
427 }
428}