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