matrixcode_core/tools/
glob.rs1use std::path::{Path, PathBuf};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde_json::{Value, json};
6
7use super::{Tool, ToolDefinition, ToolContext};
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 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!("通过 glob 模式查找文件路径。
28
29适用场景:
30- 按文件名模式查找(如 '**/*.rs'、'src/*.toml')
31- 查找特定扩展名的所有文件
32- 定位配置文件位置
33
34{}
35
36返回匹配路径,按修改时间排序(最新在前)。", prefer_section);
37
38 ToolDefinition {
39 name: "glob".to_string(),
40 description,
41 parameters: json!({
42 "type": "object",
43 "properties": {
44 "pattern": {
45 "type": "string",
46 "description": "Glob 模式,支持 '*'、'?' 和递归 '**'"
47 },
48 "path": {
49 "type": "string",
50 "description": "搜索的基础目录(默认 '.')"
51 }
52 },
53 "required": ["pattern"]
54 }),
55 ..Default::default()
56 }
57 }
58
59 fn definition(&self) -> ToolDefinition {
60 self.definition_with_context(&ToolContext::default())
61 }
62
63 async fn execute(&self, params: Value) -> Result<String> {
64 let pattern = params["pattern"]
65 .as_str()
66 .ok_or_else(|| anyhow::anyhow!("missing 'pattern'"))?
67 .to_string();
68 let path = params["path"].as_str().unwrap_or(".").to_string();
69
70 tokio::task::spawn_blocking(move || find_files(&pattern, &path)).await?
75 }
76}
77
78fn find_files(pattern: &str, path: &str) -> Result<String> {
79 let full_pattern = if path.is_empty() || path == "." {
80 pattern.to_string()
81 } else {
82 format!("{}/{}", path.trim_end_matches('/'), pattern)
83 };
84
85 let mut matches: Vec<(PathBuf, std::time::SystemTime)> = Vec::new();
86
87 for entry in glob::glob(&full_pattern)? {
88 let p = match entry {
89 Ok(p) => p,
90 Err(_) => continue,
91 };
92 if should_skip(&p) || !p.is_file() {
93 continue;
94 }
95 let mtime = std::fs::metadata(&p)
96 .and_then(|m| m.modified())
97 .unwrap_or(std::time::UNIX_EPOCH);
98 matches.push((p, mtime));
99
100 if matches.len() > MAX_RESULTS * 2 {
101 break;
102 }
103 }
104
105 matches.sort_by(|a, b| b.1.cmp(&a.1));
106
107 let total = matches.len();
108 let truncated = total > MAX_RESULTS;
109 matches.truncate(MAX_RESULTS);
110
111 if matches.is_empty() {
112 return Ok("No files matched.".to_string());
113 }
114
115 let mut out = matches
116 .into_iter()
117 .map(|(p, _)| p.display().to_string())
118 .collect::<Vec<_>>()
119 .join("\n");
120
121 if truncated {
122 out.push_str(&format!(
123 "\n... (showing {} of {}+ matches)",
124 MAX_RESULTS, total
125 ));
126 }
127
128 Ok(out)
129}
130
131fn should_skip(p: &Path) -> bool {
132 const IGNORED: &[&str] = &[".git", ".hg", ".svn", "node_modules", "target"];
133 for c in p.components() {
134 if let std::path::Component::Normal(s) = c
135 && let Some(name) = s.to_str()
136 && IGNORED.contains(&name)
137 {
138 return true;
139 }
140 }
141 false
142}