Skip to main content

matrixcode_core/tools/
glob.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde_json::{Value, json};
6
7use super::{Tool, ToolDefinition};
8
9pub struct GlobTool;
10
11const MAX_RESULTS: usize = 200;
12
13#[async_trait]
14impl Tool for GlobTool {
15    fn definition(&self) -> ToolDefinition {
16        ToolDefinition {
17            name: "glob".to_string(),
18            description:
19                "Find files by glob pattern (e.g. '**/*.rs', 'src/*.toml'). \
20                 Returns matching file paths sorted by modification time (newest first). \
21                 Use this to locate files by name; use 'search' to find content inside files."
22                    .to_string(),
23            parameters: json!({
24                "type": "object",
25                "properties": {
26                    "pattern": {
27                        "type": "string",
28                        "description": "Glob pattern, supports '*', '?', and recursive '**'"
29                    },
30                    "path": {
31                        "type": "string",
32                        "description": "Base directory to search in (defaults to '.')"
33                    }
34                },
35                "required": ["pattern"]
36            }),
37        }
38    }
39
40    async fn execute(&self, params: Value) -> Result<String> {
41        let pattern = params["pattern"]
42            .as_str()
43            .ok_or_else(|| anyhow::anyhow!("missing 'pattern'"))?
44            .to_string();
45        let path = params["path"].as_str().unwrap_or(".").to_string();
46
47        // Show spinner while globbing - RAII guard ensures cleanup on error
48        // let mut spinner = ToolSpinner::new(&format!("glob '{}' in {}", pattern, path));
49
50        
51
52        // Return result directly
53        tokio::task::spawn_blocking(move || find_files(&pattern, &path)).await?
54    }
55}
56
57fn find_files(pattern: &str, path: &str) -> Result<String> {
58    let full_pattern = if path.is_empty() || path == "." {
59        pattern.to_string()
60    } else {
61        format!("{}/{}", path.trim_end_matches('/'), pattern)
62    };
63
64    let mut matches: Vec<(PathBuf, std::time::SystemTime)> = Vec::new();
65
66    for entry in glob::glob(&full_pattern)? {
67        let p = match entry {
68            Ok(p) => p,
69            Err(_) => continue,
70        };
71        if should_skip(&p) || !p.is_file() {
72            continue;
73        }
74        let mtime = std::fs::metadata(&p)
75            .and_then(|m| m.modified())
76            .unwrap_or(std::time::UNIX_EPOCH);
77        matches.push((p, mtime));
78
79        if matches.len() > MAX_RESULTS * 2 {
80            break;
81        }
82    }
83
84    matches.sort_by(|a, b| b.1.cmp(&a.1));
85
86    let total = matches.len();
87    let truncated = total > MAX_RESULTS;
88    matches.truncate(MAX_RESULTS);
89
90    if matches.is_empty() {
91        return Ok("No files matched.".to_string());
92    }
93
94    let mut out = matches
95        .into_iter()
96        .map(|(p, _)| p.display().to_string())
97        .collect::<Vec<_>>()
98        .join("\n");
99
100    if truncated {
101        out.push_str(&format!(
102            "\n... (showing {} of {}+ matches)",
103            MAX_RESULTS, total
104        ));
105    }
106
107    Ok(out)
108}
109
110fn should_skip(p: &Path) -> bool {
111    const IGNORED: &[&str] = &[".git", ".hg", ".svn", "node_modules", "target"];
112    for c in p.components() {
113        if let std::path::Component::Normal(s) = c
114            && let Some(name) = s.to_str()
115                && IGNORED.contains(&name) {
116                    return true;
117                }
118    }
119    false
120}