Skip to main content

matrixcode_core/tools/
search.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use serde_json::{Value, json};
4
5use super::{Tool, ToolDefinition};
6
7pub struct SearchTool;
8
9#[async_trait]
10impl Tool for SearchTool {
11    fn definition(&self) -> ToolDefinition {
12        ToolDefinition {
13            name: "search".to_string(),
14            description: "在文件中搜索模式,类似 grep 功能".to_string(),
15            parameters: json!({
16                "type": "object",
17                "properties": {
18                    "pattern": {
19                        "type": "string",
20                        "description": "要搜索的正则表达式模式"
21                    },
22                    "path": {
23                        "type": "string",
24                        "description": "搜索的目录或文件路径(默认 '.')"
25                    },
26                    "glob": {
27                        "type": "string",
28                        "description": "文件过滤的 glob 模式(如 '*.rs')"
29                    }
30                },
31                "required": ["pattern"]
32            }),
33        }
34    }
35
36    async fn execute(&self, params: Value) -> Result<String> {
37        let pattern = params["pattern"]
38            .as_str()
39            .ok_or_else(|| anyhow::anyhow!("missing 'pattern'"))?;
40        let path = params["path"].as_str().unwrap_or(".");
41        let glob_pattern = params["glob"].as_str();
42
43        // Show spinner while searching - RAII guard ensures cleanup on error
44        // let mut spinner = ToolSpinner::new(&msg);
45
46        let pattern = pattern.to_string();
47        let path = path.to_string();
48        let glob_pattern = glob_pattern.map(|s| s.to_string());
49
50        tokio::task::spawn_blocking(move || search_files(&pattern, &path, glob_pattern.as_deref()))
51            .await?
52    }
53}
54
55fn search_files(pattern: &str, path: &str, glob_pattern: Option<&str>) -> Result<String> {
56    use std::fs;
57    use std::path::Path;
58
59    let regex = regex::Regex::new(pattern)?;
60    let mut results = Vec::new();
61    let root = Path::new(path);
62
63    let entries = collect_files(root, glob_pattern)?;
64
65    for file_path in entries {
66        let content = match fs::read_to_string(&file_path) {
67            Ok(c) => c,
68            Err(_) => continue,
69        };
70
71        for (line_num, line) in content.lines().enumerate() {
72            if regex.is_match(line) {
73                results.push(format!(
74                    "{}:{}: {}",
75                    file_path.display(),
76                    line_num + 1,
77                    line.trim()
78                ));
79            }
80        }
81
82        if results.len() > 200 {
83            results.push("... (truncated, too many results)".to_string());
84            break;
85        }
86    }
87
88    if results.is_empty() {
89        Ok("No matches found.".to_string())
90    } else {
91        Ok(results.join("\n"))
92    }
93}
94
95fn collect_files(
96    root: &std::path::Path,
97    glob_pattern: Option<&str>,
98) -> Result<Vec<std::path::PathBuf>> {
99    let mut files = Vec::new();
100
101    if root.is_file() {
102        files.push(root.to_path_buf());
103        return Ok(files);
104    }
105
106    let walker = walkdir(root)?;
107    let glob_matcher = glob_pattern.map(glob::Pattern::new).transpose()?;
108
109    for entry in walker {
110        if let Some(ref matcher) = glob_matcher
111            && let Some(name) = entry.file_name().and_then(|n| n.to_str())
112            && !matcher.matches(name)
113        {
114            continue;
115        }
116        files.push(entry);
117    }
118
119    Ok(files)
120}
121
122fn walkdir(root: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
123    use std::fs;
124
125    let mut files = Vec::new();
126    let mut stack = vec![root.to_path_buf()];
127
128    while let Some(dir) = stack.pop() {
129        let entries = match fs::read_dir(&dir) {
130            Ok(e) => e,
131            Err(_) => continue,
132        };
133
134        for entry in entries.flatten() {
135            let path = entry.path();
136            let name = entry.file_name();
137            let name_str = name.to_string_lossy();
138
139            if name_str.starts_with('.') || name_str == "node_modules" || name_str == "target" {
140                continue;
141            }
142
143            if path.is_dir() {
144                stack.push(path);
145            } else if path.is_file() {
146                files.push(path);
147            }
148        }
149    }
150
151    Ok(files)
152}