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, ToolContext, ToolDefinition};
8
9pub struct GlobTool;
10
11const MAX_RESULTS: usize = 200;
12
13#[async_trait]
14impl Tool for GlobTool {
15    fn definition_with_context(&self, ctx: &ToolContext) -> ToolDefinition {
16        // Dynamic description based on CodeGraph availability
17        let prefer_section = if ctx.codegraph_available {
18            "【优先使用 code_files 的场景】
19- 查看某个目录下有哪些代码文件 → code_files(更快)
20- 获取项目的文件结构概览 → code_files"
21        } else {
22            "【glob 的适用场景】
23- 查找非代码文件(配置文件、文档等)
24- 搜索特定命名模式的文件"
25        };
26
27        let description = format!(
28            "通过 glob 模式查找文件路径。
29
30适用场景:
31- 按文件名模式查找(如 '**/*.rs'、'src/*.toml')
32- 查找特定扩展名的所有文件
33- 定位配置文件位置
34
35{}
36
37返回匹配路径,按修改时间排序(最新在前)。",
38            prefer_section
39        );
40
41        ToolDefinition {
42            name: "glob".to_string(),
43            description,
44            parameters: json!({
45                "type": "object",
46                "properties": {
47                    "pattern": {
48                        "type": "string",
49                        "description": "Glob 模式,支持 '*'、'?' 和递归 '**'"
50                    },
51                    "path": {
52                        "type": "string",
53                        "description": "搜索的基础目录(默认 '.')"
54                    }
55                },
56                "required": ["pattern"]
57            }),
58            ..Default::default()
59        }
60    }
61
62    fn definition(&self) -> ToolDefinition {
63        self.definition_with_context(&ToolContext::default())
64    }
65
66    async fn execute(&self, params: Value) -> Result<String> {
67        let pattern = params["pattern"]
68            .as_str()
69            .ok_or_else(|| anyhow::anyhow!("missing 'pattern'"))?
70            .to_string();
71        let path = params["path"].as_str().unwrap_or(".").to_string();
72
73        // Show spinner while globbing - RAII guard ensures cleanup on error
74        // let mut spinner = ToolSpinner::new(&format!("glob '{}' in {}", pattern, path));
75
76        // Return result directly
77        tokio::task::spawn_blocking(move || find_files(&pattern, &path)).await?
78    }
79}
80
81fn find_files(pattern: &str, path: &str) -> Result<String> {
82    let full_pattern = if path.is_empty() || path == "." {
83        pattern.to_string()
84    } else {
85        format!("{}/{}", path.trim_end_matches('/'), pattern)
86    };
87
88    let mut matches: Vec<(PathBuf, std::time::SystemTime)> = Vec::new();
89
90    for entry in glob::glob(&full_pattern)? {
91        let p = match entry {
92            Ok(p) => p,
93            Err(_) => continue,
94        };
95        if should_skip(&p) || !p.is_file() {
96            continue;
97        }
98        let mtime = std::fs::metadata(&p)
99            .and_then(|m| m.modified())
100            .unwrap_or(std::time::UNIX_EPOCH);
101        matches.push((p, mtime));
102
103        if matches.len() > MAX_RESULTS * 2 {
104            break;
105        }
106    }
107
108    matches.sort_by(|a, b| b.1.cmp(&a.1));
109
110    let total = matches.len();
111    let truncated = total > MAX_RESULTS;
112    matches.truncate(MAX_RESULTS);
113
114    if matches.is_empty() {
115        return Ok("No files matched.".to_string());
116    }
117
118    let mut out = matches
119        .into_iter()
120        .map(|(p, _)| p.display().to_string())
121        .collect::<Vec<_>>()
122        .join("\n");
123
124    if truncated {
125        out.push_str(&format!(
126            "\n... (showing {} of {}+ matches)",
127            MAX_RESULTS, total
128        ));
129    }
130
131    Ok(out)
132}
133
134fn should_skip(p: &Path) -> bool {
135    const IGNORED: &[&str] = &[".git", ".hg", ".svn", "node_modules", "target"];
136    for c in p.components() {
137        if let std::path::Component::Normal(s) = c
138            && let Some(name) = s.to_str()
139            && IGNORED.contains(&name)
140        {
141            return true;
142        }
143    }
144    false
145}