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