mod engine;
mod matcher;
mod reader;
use super::{
ToolResult, ToolResultDisplay,
args::{GrepArgs, GrepMode},
contract::tool_name,
};
use crate::cancellation::AgentCancellation;
use serde_json::json;
pub(in crate::tools) struct GrepContext<'a> {
pub(in crate::tools) paths: crate::tools::fs::PathContext<'a>,
pub(in crate::tools) settings: &'a crate::config::GrepToolSettings,
pub(in crate::tools) workspace_walker: &'a super::workspace::WorkspaceWalker,
}
impl GrepContext<'_> {
pub(super) fn grep(
&self,
args: GrepArgs,
cancellation: &AgentCancellation,
) -> anyhow::Result<ToolResult> {
let path_arg = args.path.clone().unwrap_or_else(|| ".".into());
let path = self.paths.resolve_existing_path(
&path_arg,
super::fs::ExistingPathPolicy::grep(self.settings.absolute_paths),
)?;
cancellation.check()?;
let mode = args.mode.unwrap_or(GrepMode::Ranked);
let raw = mode == GrepMode::Raw || args.pattern.is_some();
let limit = args.limit.unwrap_or(crate::tools::args::GREP_DEFAULT_LIMIT);
let offset = args.offset.unwrap_or(0);
let requested_context = args.context.unwrap_or(0);
let context = requested_context.min(super::args::GREP_MAX_CONTEXT);
let notice = (requested_context != context)
.then(|| format!("context clamped from {requested_context} to {context}"));
let scan = engine::run(engine::GrepRequest {
cwd: self.paths.cwd_canonical,
walker: self.workspace_walker,
patterns: args
.patterns
.as_ref()
.ok_or_else(|| anyhow::anyhow!("patterns required"))?,
root: &path,
limit,
offset,
context,
notice: notice.as_deref(),
raw,
cancellation,
})?;
let pattern_count = args.patterns.as_ref().map_or(0, Vec::len);
let mut metadata = json!({
"engine": "native",
"patterns_count": pattern_count,
"mode": if raw { "raw" } else { "ranked" },
"offset": offset,
"limit": limit,
"output_format": if raw { "raw_v1" } else { "grouped_v1" },
"order": if raw { "path_line_v1" } else { "ranked_v1" },
"matches_returned": scan.matches_returned,
"output_lines_returned": scan.output_lines_returned,
"truncated": scan.truncated,
"timed_out": scan.timed_out,
"files_scanned": scan.files_scanned,
"bytes_scanned": scan.bytes_scanned,
"byte_limit_reached": scan.byte_limit_reached,
"scan_byte_limit_reached": scan.scan_byte_limit_reached,
"file_limit_reached": scan.file_limit_reached,
"match_limit_reached": scan.match_limit_reached,
"per_file_limit_reached": scan.per_file_limit_reached,
"files_with_matches": scan.files_with_matches,
"files_returned": scan.files_returned,
"invalid_utf8_files": scan.invalid_utf8_files,
"invalid_utf8_lines": scan.invalid_utf8_lines,
"read_errors_skipped": scan.read_errors_skipped,
"walk_errors": scan.walk_errors,
"walk_entries_omitted": scan.walk_entries_omitted,
"binary_files_skipped": scan.binary_files_skipped,
"scan_complete": scan.scan_complete,
"matches_seen": scan.matches_seen,
"matches_seen_exact": scan.matches_seen_exact,
"output_bytes": scan.output_bytes,
"output_byte_limit_reached": scan.output_byte_limit_reached,
"context_lines_omitted": scan.context_lines_omitted,
"patterns_adjusted": scan.patterns_adjusted,
"literal_fallbacks": scan.literal_fallbacks,
"raw_lookahead_reached": scan.raw_lookahead_reached,
"truncation_reasons": scan.truncation_reasons,
"limit_reason": scan.truncation_reasons.first().copied(),
});
metadata["next_offset"] = scan
.next_offset
.map_or(serde_json::Value::Null, |v| json!(v));
metadata["has_more"] = scan.has_more.map_or(serde_json::Value::Null, |v| json!(v));
let mut content = scan.lines.join("\n");
if scan.matches_returned == 0 && !scan.scan_complete {
if !content.is_empty() {
content.push('\n');
}
content.push_str(&format!(
"SEARCH INCOMPLETE: no matching lines returned; search coverage incomplete ({})",
scan.truncation_reasons.join(", ")
));
}
metadata["output_bytes"] = json!(content.len());
Ok(ToolResult {
tool_name: tool_name::GREP.into(),
success: true,
content,
metadata,
display: ToolResultDisplay::default(),
})
}
}