use crate::command::chat::tools::{
PlanDecision, Tool, ToolResult, effective_cwd, parse_tool_args, resolve_path,
schema_to_tool_params,
};
use schemars::JsonSchema;
use serde::Deserialize;
use serde_json::Value;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
#[derive(Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
struct GlobParams {
pattern: String,
#[serde(default)]
path: Option<String>,
#[serde(default = "default_limit")]
limit: usize,
#[serde(default)]
offset: usize,
#[serde(default)]
exclude_pattern: Option<String>,
}
fn default_limit() -> usize {
100
}
pub struct GlobTool;
impl Tool for GlobTool {
fn name(&self) -> &str {
"Glob"
}
fn description(&self) -> &str {
r###"
- Fast file pattern matching tool, works with any codebase size
- Supports glob patterns like "**/*.js" or "src/**/*.tsx"
- Returns matching file paths sorted by modification time
- Use this tool when you need to find files by name pattern, e.g. "src/components/**/*.tsx"
- When you are doing an open-ended search that may require multiple rounds of glob and grep, use the Agent tool instead
- You can call multiple tools in a single response. If multiple file patterns may be useful, run glob searches in parallel
- Important: if no path is needed, omit the field entirely — do not enter "undefined", "null", or empty string
"###
}
fn parameters_schema(&self) -> Value {
schema_to_tool_params::<GlobParams>()
}
fn execute(&self, arguments: &str, _cancelled: &Arc<AtomicBool>) -> ToolResult {
let params: GlobParams = match parse_tool_args(arguments) {
Ok(p) => p,
Err(e) => return e,
};
let base_path = params
.path
.as_deref()
.filter(|s| !s.is_empty())
.map(resolve_path)
.unwrap_or_else(effective_cwd);
let limit = params.limit.clamp(1, 1000);
let exclude_pattern = params
.exclude_pattern
.as_deref()
.filter(|s| !s.is_empty())
.and_then(|p| glob::Pattern::new(p).ok());
let full_pattern = if params.pattern.starts_with('/') {
params.pattern.clone()
} else {
format!("{}/{}", base_path.trim_end_matches('/'), params.pattern)
};
let mut matches: Vec<std::path::PathBuf> = match glob::glob(&full_pattern) {
Ok(paths) => paths
.filter_map(Result::ok)
.filter(|path| {
if let Some(ref exclude) = exclude_pattern {
let path_str = path.to_string_lossy();
!exclude.matches(&path_str)
} else {
true
}
})
.collect(),
Err(e) => {
return ToolResult {
output: format!("glob 模式无效: {}", e),
is_error: true,
images: vec![],
plan_decision: PlanDecision::None,
};
}
};
matches.sort_by(|a, b| {
let meta_a = std::fs::metadata(a);
let meta_b = std::fs::metadata(b);
let time_a = meta_a.ok().and_then(|m| m.modified().ok());
let time_b = meta_b.ok().and_then(|m| m.modified().ok());
time_b.cmp(&time_a)
});
let total = matches.len();
if total == 0 {
return ToolResult {
output: format!("未找到匹配 '{}' 的文件", params.pattern),
is_error: false,
images: vec![],
plan_decision: PlanDecision::None,
};
}
let paginated: Vec<_> = matches
.into_iter()
.skip(params.offset)
.take(limit)
.collect();
let displayed = paginated.len();
let mut result = String::new();
result.push_str(&format!("找到 {} 个匹配文件", total));
if params.offset > 0 || params.offset + displayed < total {
result.push_str(&format!(
"(显示 {}-{} 项,共 {} 项)",
params.offset + 1,
params.offset + displayed,
total
));
}
result.push_str(":\n\n");
for path in paginated {
result.push_str(&format!("{}\n", path.display()));
}
if params.offset + displayed < total {
result.push_str(&format!(
"\n... 还有 {} 个结果未显示(使用 offset={} 继续查看)",
total - params.offset - displayed,
params.offset + displayed
));
}
ToolResult {
output: result,
is_error: false,
images: vec![],
plan_decision: PlanDecision::None,
}
}
fn requires_confirmation(&self) -> bool {
false
}
}