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: "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        // Show spinner while searching - RAII guard ensures cleanup on error
42        // let mut spinner = ToolSpinner::new(&msg);
43
44        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}