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: "Search for a pattern in files using grep-like functionality".to_string(),
15 parameters: json!({
16 "type": "object",
17 "properties": {
18 "pattern": {
19 "type": "string",
20 "description": "The regex pattern to search for"
21 },
22 "path": {
23 "type": "string",
24 "description": "Directory or file path to search in (defaults to '.')"
25 },
26 "glob": {
27 "type": "string",
28 "description": "File glob pattern to filter files (e.g. '*.rs')"
29 }
30 },
31 "required": ["pattern"]
32 }),
33 }
34 }
35
36 async fn execute(&self, params: Value) -> Result<String> {
37 let pattern = params["pattern"].as_str().ok_or_else(|| anyhow::anyhow!("missing 'pattern'"))?;
38 let path = params["path"].as_str().unwrap_or(".");
39 let glob_pattern = params["glob"].as_str();
40
41 let pattern = pattern.to_string();
45 let path = path.to_string();
46 let glob_pattern = glob_pattern.map(|s| s.to_string());
47
48
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!("{}:{}: {}", file_path.display(), line_num + 1, line.trim()));
74 }
75 }
76
77 if results.len() > 200 {
78 results.push("... (truncated, too many results)".to_string());
79 break;
80 }
81 }
82
83 if results.is_empty() {
84 Ok("No matches found.".to_string())
85 } else {
86 Ok(results.join("\n"))
87 }
88}
89
90fn collect_files(root: &std::path::Path, glob_pattern: Option<&str>) -> Result<Vec<std::path::PathBuf>> {
91 let mut files = Vec::new();
92
93 if root.is_file() {
94 files.push(root.to_path_buf());
95 return Ok(files);
96 }
97
98 let walker = walkdir(root)?;
99 let glob_matcher = glob_pattern.map(glob::Pattern::new).transpose()?;
100
101 for entry in walker {
102 if let Some(ref matcher) = glob_matcher
103 && let Some(name) = entry.file_name().and_then(|n| n.to_str())
104 && !matcher.matches(name) {
105 continue;
106 }
107 files.push(entry);
108 }
109
110 Ok(files)
111}
112
113fn walkdir(root: &std::path::Path) -> Result<Vec<std::path::PathBuf>> {
114 use std::fs;
115
116 let mut files = Vec::new();
117 let mut stack = vec![root.to_path_buf()];
118
119 while let Some(dir) = stack.pop() {
120 let entries = match fs::read_dir(&dir) {
121 Ok(e) => e,
122 Err(_) => continue,
123 };
124
125 for entry in entries.flatten() {
126 let path = entry.path();
127 let name = entry.file_name();
128 let name_str = name.to_string_lossy();
129
130 if name_str.starts_with('.') || name_str == "node_modules" || name_str == "target" {
131 continue;
132 }
133
134 if path.is_dir() {
135 stack.push(path);
136 } else if path.is_file() {
137 files.push(path);
138 }
139 }
140 }
141
142 Ok(files)
143}