Skip to main content

deepseek_rust_cli/tools/
file_ops.rs

1use std::fs;
2
3use anyhow::Result;
4use md5;
5use sha2::{Digest, Sha256};
6use tokio::task;
7use walkdir::WalkDir;
8
9use crate::tools::base::validate_path;
10
11pub async fn tree_view(path: Option<String>, max_depth: Option<usize>) -> Result<String> {
12    task::spawn_blocking(move || {
13        let dir = path.unwrap_or_else(|| ".".to_string());
14        let _ = validate_path(&dir)?;
15        let depth = max_depth.unwrap_or(3);
16        let mut output = String::new();
17        for entry in WalkDir::new(dir).max_depth(depth).into_iter().flatten() {
18            let indent = "  ".repeat(entry.depth());
19            output.push_str(&format!(
20                "{}{}\n",
21                indent,
22                entry.file_name().to_string_lossy()
23            ));
24        }
25        Ok(output)
26    })
27    .await?
28}
29
30pub async fn diff_files(file1: &str, file2: &str) -> Result<String> {
31    let p1 = validate_path(file1)?;
32    let p2 = validate_path(file2)?;
33
34    let content1 = tokio::fs::read_to_string(p1).await?;
35    let content2 = tokio::fs::read_to_string(p2).await?;
36
37    let mut output = String::new();
38    for r in diff::lines(&content1, &content2) {
39        match r {
40            diff::Result::Left(l) => {
41                output.push_str("- ");
42                output.push_str(l);
43                output.push('\n');
44            }
45            diff::Result::Both(l, _) => {
46                output.push_str("  ");
47                output.push_str(l);
48                output.push('\n');
49            }
50            diff::Result::Right(r) => {
51                output.push_str("+ ");
52                output.push_str(r);
53                output.push('\n');
54            }
55        }
56    }
57    Ok(output)
58}
59
60pub async fn hash_file(path: String, algorithm: Option<String>) -> Result<String> {
61    let p = validate_path(&path)?;
62    task::spawn_blocking(move || {
63        let content = fs::read(p)?;
64        let alg = algorithm.unwrap_or_else(|| "sha256".to_string());
65        if alg == "md5" {
66            Ok(format!("{:x}", md5::compute(content)))
67        } else {
68            let mut hasher = Sha256::new();
69            hasher.update(content);
70            Ok(format!("{:x}", hasher.finalize()))
71        }
72    })
73    .await?
74}
75
76pub async fn count_lines(path: String) -> Result<String> {
77    let p = validate_path(&path)?;
78    task::spawn_blocking(move || {
79        let content = fs::read_to_string(p)?;
80        let lines = content.lines().count();
81        let words = content.split_whitespace().count();
82        let chars = content.len();
83        Ok(format!(
84            "Lines: {}, Words: {}, Chars: {}",
85            lines, words, chars
86        ))
87    })
88    .await?
89}
90
91pub async fn move_code_block(
92    src_path: &str,
93    dst_path: &str,
94    block_pattern: &str,
95) -> Result<String> {
96    let sp = validate_path(src_path)?;
97    let dp = validate_path(dst_path)?;
98
99    let src_content = tokio::fs::read_to_string(&sp).await?;
100    let mut dst_content = tokio::fs::read_to_string(&dp).await.unwrap_or_default();
101
102    let re = regex::Regex::new(block_pattern)?;
103    if let Some(mat) = re.find(&src_content) {
104        let block = mat.as_str().to_string();
105        let new_src = src_content.replace(&block, "");
106
107        // Append to destination
108        if !dst_content.is_empty() && !dst_content.ends_with('\n') {
109            dst_content.push('\n');
110        }
111        dst_content.push_str(&block);
112        dst_content.push('\n');
113
114        tokio::fs::write(sp, new_src).await?;
115        tokio::fs::write(dp, dst_content).await?;
116
117        Ok(format!(
118            "Moved code block matching '{}' from {} to {}.",
119            block_pattern, src_path, dst_path
120        ))
121    } else {
122        Err(anyhow::anyhow!("Code block not found in source file."))
123    }
124}
125
126pub async fn view_symbol_contents(path: &str, symbol_name: &str) -> Result<String> {
127    let p = validate_path(path)?;
128    let content = tokio::fs::read_to_string(&p).await?;
129    let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("");
130
131    let lines: Vec<&str> = content.lines().collect();
132    let mut start_line_idx = None;
133
134    match ext {
135        "rs" | "go" | "js" | "ts" | "java" | "c" | "cpp" | "h" | "hpp" | "cs" => {
136            let patterns = vec![
137                format!(
138                    r"(?m)^(?:\s*pub\s+)?(?:async\s+)?fn\s+{}\b",
139                    regex::escape(symbol_name)
140                ),
141                format!(
142                    r"(?m)^(?:\s*pub\s+)?struct\s+{}\b",
143                    regex::escape(symbol_name)
144                ),
145                format!(
146                    r"(?m)^(?:\s*pub\s+)?class\s+{}\b",
147                    regex::escape(symbol_name)
148                ),
149                format!(
150                    r"(?m)^(?:\s*pub\s+)?enum\s+{}\b",
151                    regex::escape(symbol_name)
152                ),
153                format!(
154                    r"(?m)^(?:\s*pub\s+)?impl(?:\s*<.*>)?\s+{}\b",
155                    regex::escape(symbol_name)
156                ),
157                format!(
158                    r"(?m)^(?:\s*pub\s+)?type\s+{}\b",
159                    regex::escape(symbol_name)
160                ),
161                format!(
162                    r"(?m)^(?:\s*pub\s+)?trait\s+{}\b",
163                    regex::escape(symbol_name)
164                ),
165            ];
166
167            for pattern in patterns {
168                if let Ok(re) = regex::Regex::new(&pattern) {
169                    for (idx, line) in lines.iter().enumerate() {
170                        if re.is_match(line) {
171                            start_line_idx = Some(idx);
172                            break;
173                        }
174                    }
175                }
176                if start_line_idx.is_some() {
177                    break;
178                }
179            }
180
181            if let Some(start_idx) = start_line_idx {
182                let mut brace_count = 0;
183                let mut seen_brace = false;
184                let mut end_line_idx = start_idx;
185
186                for (idx, line) in lines.iter().enumerate().skip(start_idx) {
187                    end_line_idx = idx;
188                    for c in line.chars() {
189                        if c == '{' {
190                            brace_count += 1;
191                            seen_brace = true;
192                        } else if c == '}' {
193                            brace_count -= 1;
194                        }
195                    }
196                    if seen_brace && brace_count <= 0 {
197                        break;
198                    }
199                }
200
201                let result_lines = &lines[start_idx..=end_line_idx];
202                return Ok(result_lines.join("\n"));
203            }
204        }
205        "py" => {
206            let patterns = vec![
207                format!(
208                    r"(?m)^\s*(?:async\s+)?def\s+{}\b",
209                    regex::escape(symbol_name)
210                ),
211                format!(r"(?m)^\s*class\s+{}\b", regex::escape(symbol_name)),
212            ];
213
214            for pattern in patterns {
215                if let Ok(re) = regex::Regex::new(&pattern) {
216                    for (idx, line) in lines.iter().enumerate() {
217                        if re.is_match(line) {
218                            start_line_idx = Some(idx);
219                            break;
220                        }
221                    }
222                }
223                if start_line_idx.is_some() {
224                    break;
225                }
226            }
227
228            if let Some(start_idx) = start_line_idx {
229                let start_line = lines[start_idx];
230                let start_indent = start_line.len() - start_line.trim_start().len();
231                let mut end_line_idx = start_idx;
232
233                for (idx, line) in lines.iter().enumerate().skip(start_idx + 1) {
234                    let trimmed = line.trim();
235                    if trimmed.is_empty() {
236                        continue;
237                    }
238                    let indent = line.len() - line.trim_start().len();
239                    if indent <= start_indent {
240                        break;
241                    }
242                    end_line_idx = idx;
243                }
244
245                let result_lines = &lines[start_idx..=end_line_idx];
246                return Ok(result_lines.join("\n"));
247            }
248        }
249        _ => {}
250    }
251
252    Err(anyhow::anyhow!(
253        "Symbol '{}' not found in file (or unsupported file extension: {})",
254        symbol_name,
255        ext
256    ))
257}