matrixcode_core/tools/
search.rs1use 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 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}