mod engine;
mod matcher;
mod reader;
use super::{
ToolResult, ToolResultDisplay, ToolRuntime,
args::GrepArgs,
contract::{metadata_key as meta, tool_name},
};
use crate::agent::cancellation::AgentCancellation;
use serde_json::json;
use std::path::Path;
impl ToolRuntime {
pub(super) fn grep(
&self,
args: GrepArgs,
cancellation: &AgentCancellation,
) -> anyhow::Result<ToolResult> {
let path_arg = args.path.clone().unwrap_or_else(|| ".".to_string());
let path = self.resolve_existing_path(
&path_arg,
super::fs::ExistingPathPolicy::grep(self.grep_absolute_paths),
)?;
cancellation.check()?;
let scan = engine::run(engine::GrepRequest {
cwd: &self.cwd_canonical,
walker: &self.workspace_walker,
pattern: &args.pattern,
root: &path,
raw_limit: args.limit,
context: args.context.unwrap_or(0),
cancellation,
})?;
Ok(ToolResult {
tool_name: tool_name::GREP.to_string(),
success: !scan.timed_out,
content: scan.matches.join("\n"),
metadata: json!({
(meta::ENGINE): "native",
(meta::MATCHES_RETURNED): scan.matches.len(),
(meta::TRUNCATED): scan.truncated,
(meta::TIMED_OUT): scan.timed_out,
(meta::FILES_SCANNED): scan.files_scanned,
(meta::BYTES_SCANNED): scan.bytes_scanned,
(meta::LIMIT_REASON): scan.limit_reason,
}),
display: ToolResultDisplay::default(),
})
}
}
pub(super) fn push_grep_match(
matches: &mut Vec<String>,
cwd: &Path,
file: &Path,
line_no: usize,
line: &str,
) {
matches.push(format!(
"{}:{}:{}",
file.strip_prefix(cwd).unwrap_or(file).display(),
line_no,
line.trim_end_matches(['\r', '\n'])
));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
agent::cancellation::AgentCancellation,
tools::{ToolRuntime, args::ListFilesArgs},
};
use std::fs;
fn grep(
runtime: &ToolRuntime,
pattern: &str,
limit: Option<usize>,
context: Option<usize>,
) -> ToolResult {
runtime
.grep(
GrepArgs {
pattern: pattern.to_string(),
path: Some(".".to_string()),
limit,
context,
},
&AgentCancellation::default(),
)
.unwrap()
}
#[test]
fn native_grep_excludes_gitignored_files_without_contaminating_list_files_cache() {
let temp = tempfile::TempDir::new().unwrap();
fs::create_dir(temp.path().join(".git")).unwrap();
fs::write(temp.path().join(".gitignore"), "ignored.txt\n").unwrap();
fs::write(temp.path().join("ignored.txt"), "needle").unwrap();
fs::write(temp.path().join("visible.txt"), "needle").unwrap();
let runtime = ToolRuntime::new(temp.path()).unwrap();
let result = grep(&runtime, "needle", Some(10), None);
let list_result = runtime
.list_files(ListFilesArgs {
path: ".".to_string(),
include_directories: false,
})
.unwrap();
assert!(result.content.contains("visible.txt:1:needle"));
assert!(!result.content.contains("ignored.txt"));
assert!(list_result.content.contains("ignored.txt"));
}
#[test]
fn native_metadata_and_output_shape_are_preserved() {
let temp = tempfile::TempDir::new().unwrap();
fs::write(temp.path().join("a.txt"), "hay\nneedle\n").unwrap();
let runtime = ToolRuntime::new(temp.path()).unwrap();
let result = grep(&runtime, "needle", Some(5), None);
assert_eq!(result.tool_name, "grep");
assert_eq!(result.content, "a.txt:2:needle");
assert_eq!(result.metadata["engine"], "native");
assert!(result.metadata.get("exit_code").is_none());
assert!(result.metadata.get("stderr").is_none());
assert!(result.metadata.get("cleanup_warning").is_none());
}
#[test]
fn native_context_lines_use_same_content_shape() {
let temp = tempfile::TempDir::new().unwrap();
fs::write(temp.path().join("a.txt"), "before\nneedle\nafter\n").unwrap();
let runtime = ToolRuntime::new(temp.path()).unwrap();
let result = grep(&runtime, "needle", Some(5), Some(1));
assert_eq!(
result.content,
"a.txt:1:before\na.txt:2:needle\na.txt:3:after"
);
}
#[test]
fn default_limit_caps_parallel_results_at_100() {
let temp = tempfile::TempDir::new().unwrap();
for index in 0..120 {
fs::write(temp.path().join(format!("{index:03}.txt")), "needle\n").unwrap();
}
let runtime = ToolRuntime::new(temp.path()).unwrap();
let result = grep(&runtime, "needle", None, None);
assert_eq!(result.content.lines().count(), 100);
assert_eq!(result.metadata["truncated"], true);
assert_eq!(result.metadata["limit_reason"], "matches");
}
#[test]
fn line_anchors_are_line_oriented() {
let temp = tempfile::TempDir::new().unwrap();
fs::write(temp.path().join("a.txt"), "xneedle\nneedle\nneedleX\n").unwrap();
let runtime = ToolRuntime::new(temp.path()).unwrap();
let result = grep(&runtime, "^needle$", Some(10), None);
assert_eq!(result.content, "a.txt:2:needle");
}
}