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: "通过 glob 模式查找文件(如 '**/*.rs'、'src/*.toml')。\
19                 返回匹配的文件路径,按修改时间排序(最新的在前)。\
20                 用于按名称定位文件;若要查找文件内容请使用 'search'。"
21                .to_string(),
22            parameters: json!({
23                "type": "object",
24                "properties": {
25                    "pattern": {
26                        "type": "string",
27                        "description": "Glob 模式,支持 '*'、'?' 和递归 '**'"
28                    },
29                    "path": {
30                        "type": "string",
31                        "description": "搜索的基础目录(默认 '.')"
32                    }
33                },
34                "required": ["pattern"]
35            }),
36        }
37    }
38
39    async fn execute(&self, params: Value) -> Result<String> {
40        let pattern = params["pattern"]
41            .as_str()
42            .ok_or_else(|| anyhow::anyhow!("missing 'pattern'"))?
43            .to_string();
44        let path = params["path"].as_str().unwrap_or(".").to_string();
45
46        // Show spinner while globbing - RAII guard ensures cleanup on error
47        // let mut spinner = ToolSpinner::new(&format!("glob '{}' in {}", pattern, path));
48
49        // Return result directly
50        tokio::task::spawn_blocking(move || find_files(&pattern, &path)).await?
51    }
52}
53
54fn find_files(pattern: &str, path: &str) -> Result<String> {
55    let full_pattern = if path.is_empty() || path == "." {
56        pattern.to_string()
57    } else {
58        format!("{}/{}", path.trim_end_matches('/'), pattern)
59    };
60
61    let mut matches: Vec<(PathBuf, std::time::SystemTime)> = Vec::new();
62
63    for entry in glob::glob(&full_pattern)? {
64        let p = match entry {
65            Ok(p) => p,
66            Err(_) => continue,
67        };
68        if should_skip(&p) || !p.is_file() {
69            continue;
70        }
71        let mtime = std::fs::metadata(&p)
72            .and_then(|m| m.modified())
73            .unwrap_or(std::time::UNIX_EPOCH);
74        matches.push((p, mtime));
75
76        if matches.len() > MAX_RESULTS * 2 {
77            break;
78        }
79    }
80
81    matches.sort_by(|a, b| b.1.cmp(&a.1));
82
83    let total = matches.len();
84    let truncated = total > MAX_RESULTS;
85    matches.truncate(MAX_RESULTS);
86
87    if matches.is_empty() {
88        return Ok("No files matched.".to_string());
89    }
90
91    let mut out = matches
92        .into_iter()
93        .map(|(p, _)| p.display().to_string())
94        .collect::<Vec<_>>()
95        .join("\n");
96
97    if truncated {
98        out.push_str(&format!(
99            "\n... (showing {} of {}+ matches)",
100            MAX_RESULTS, total
101        ));
102    }
103
104    Ok(out)
105}
106
107fn should_skip(p: &Path) -> bool {
108    const IGNORED: &[&str] = &[".git", ".hg", ".svn", "node_modules", "target"];
109    for c in p.components() {
110        if let std::path::Component::Normal(s) = c
111            && let Some(name) = s.to_str()
112            && IGNORED.contains(&name)
113        {
114            return true;
115        }
116    }
117    false
118}