Skip to main content

deepseek_rust_cli/tools/file/
ops.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 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        // Support undo
302        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}