use crate::core::{Tool, ToolArgs, ToolError, ToolResult};
use anyhow::Result;
use ignore::{overrides::OverrideBuilder, WalkBuilder};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
const LIMIT: usize = 100;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GlobParams {
pub pattern: String,
pub path: Option<String>,
}
pub struct GlobTool {
name: String,
}
impl GlobTool {
pub fn new() -> Self {
Self {
name: "glob".to_string(),
}
}
}
impl Default for GlobTool {
fn default() -> Self {
Self::new()
}
}
impl Tool for GlobTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Fast file pattern matching tool that works with any codebase size. Uses gitignore/wildmatch syntax (e.g. **/*.rs, **/src/**, *.toml). 'path' is optional — defaults to current working directory. Supports recursive wildcards like **/dir/**."
}
fn signature(&self) -> &str {
"glob --path <directory> --pattern <pattern>"
}
fn validate_args(&self, _args: &ToolArgs) -> Result<(), ToolError> {
Ok(())
}
fn execute(
&mut self,
args: &ToolArgs,
state: &Arc<Mutex<crate::state::ToolState>>,
) -> Result<ToolResult> {
let params = parse_glob_args(args)?;
let working_dir = state
.lock()
.map(|s| s.working_directory.clone())
.unwrap_or_else(|_| std::env::current_dir().unwrap_or_default());
let search_path = match ¶ms.path {
Some(p) => {
let pb = PathBuf::from(p);
if pb.is_absolute() { pb } else { working_dir.join(p) }
}
None => working_dir.clone(),
};
let mut files: Vec<(PathBuf, std::time::SystemTime)> = Vec::new();
let mut truncated = false;
let mut ob = OverrideBuilder::new(&search_path);
ob.add(¶ms.pattern)
.map_err(|e| anyhow::anyhow!("Invalid glob pattern: {}", e))?;
let overrides = ob.build()
.map_err(|e| anyhow::anyhow!("Failed to build glob override: {}", e))?;
let walker = WalkBuilder::new(&search_path)
.hidden(false) .git_ignore(false) .git_global(false)
.git_exclude(false)
.ignore(false) .overrides(overrides)
.build();
for result in walker {
if files.len() >= LIMIT {
truncated = true;
break;
}
if let Ok(entry) = result {
if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false) {
let path = entry.path().to_path_buf();
let mtime = fs::metadata(&path)
.ok()
.and_then(|m| m.modified().ok())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
files.push((path, mtime));
}
}
}
files.sort_by(|a, b| b.1.cmp(&a.1));
let file_count = files.len();
let mut output_lines: Vec<String> = Vec::new();
if files.is_empty() {
output_lines.push("No files found".to_string());
} else {
for (path, _) in &files {
output_lines.push(path.display().to_string());
}
if truncated {
output_lines.push("".to_string());
output_lines.push(
"(Results are truncated. Consider using a more specific path or pattern.)"
.to_string(),
);
}
}
Ok(ToolResult::success_with_data(
output_lines.join("\n"),
serde_json::json!({
"count": file_count,
"truncated": truncated,
"pattern": params.pattern,
"path": search_path.display().to_string(),
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
let schema = schemars::schema_for!(GlobParams);
serde_json::to_value(schema).unwrap_or_default()
}
}
fn parse_glob_args(args: &ToolArgs) -> Result<GlobParams> {
let pattern = args
.get_named_arg("pattern")
.cloned()
.filter(|s| !s.is_empty())
.or_else(|| args.args.first().cloned())
.filter(|s| !s.is_empty())
.ok_or_else(|| anyhow::anyhow!("pattern is required and must be non-empty"))?;
if pattern.is_empty() {
return Err(anyhow::anyhow!("pattern must not be empty or whitespace-only"));
}
let path = args
.get_named_arg("path")
.cloned()
.filter(|s| !s.is_empty());
Ok(GlobParams { pattern, path })
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_glob_tool_creation() {
let tool = GlobTool::new();
assert_eq!(tool.name(), "glob");
}
#[test]
fn test_glob_tool_find_files() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("test1.txt"), "").unwrap();
fs::write(temp_dir.path().join("test2.txt"), "").unwrap();
fs::write(temp_dir.path().join("test.rs"), "").unwrap();
let mut tool = GlobTool::new();
let state = Arc::new(Mutex::new(crate::state::ToolState::new()));
let args = ToolArgs::with_named_args(
vec!["*.txt".to_string()],
vec![(
"path".to_string(),
temp_dir.path().to_str().unwrap().to_string(),
)]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
assert!(result.message.contains("test1.txt"));
assert!(result.message.contains("test2.txt"));
assert!(!result.message.contains("test.rs"));
}
#[test]
fn test_glob_tool_no_matches() {
let temp_dir = TempDir::new().unwrap();
let mut tool = GlobTool::new();
let state = Arc::new(Mutex::new(crate::state::ToolState::new()));
let args = ToolArgs::with_named_args(
vec!["*.nonexistent".to_string()],
vec![(
"path".to_string(),
temp_dir.path().to_str().unwrap().to_string(),
)]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
assert!(result.message.contains("No files found"));
}
#[test]
fn test_glob_tool_nested_pattern() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("src/nested")).unwrap();
fs::write(temp_dir.path().join("src/main.rs"), "").unwrap();
fs::write(temp_dir.path().join("src/nested/mod.rs"), "").unwrap();
let mut tool = GlobTool::new();
let state = Arc::new(Mutex::new(crate::state::ToolState::new()));
let args = ToolArgs::with_named_args(
vec!["**/*.rs".to_string()],
vec![(
"path".to_string(),
temp_dir.path().to_str().unwrap().to_string(),
)]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
assert!(result.message.contains("main.rs"));
assert!(result.message.contains("mod.rs"));
}
#[test]
fn test_glob_wildmatch_dir_pattern() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("nghr/src")).unwrap();
fs::write(temp_dir.path().join("nghr/src/main.rs"), "").unwrap();
fs::write(temp_dir.path().join("other.rs"), "").unwrap();
let mut tool = GlobTool::new();
let state = Arc::new(Mutex::new(crate::state::ToolState::new()));
let args = ToolArgs::with_named_args(
vec!["**/nghr/**".to_string()],
vec![("path".to_string(), temp_dir.path().to_str().unwrap().to_string())]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
assert!(result.message.contains("main.rs"));
assert!(!result.message.contains("other.rs"));
}
#[test]
fn test_glob_tool_validation() {
let tool = GlobTool::new();
let args = ToolArgs::from_args(&[]);
let _ = tool.validate_args(&args);
}
}