grep — searching file contents
grep finds lines of file content matching a regex. It answers “which line
talks about X?” and — because every matched file carries a hashline anchor —
gives you the tag you need to edit the match without re-reading it.
Find::grep(&self, pattern: &str, path: &str) -> Result<GrepOutput, String>
For one directory this is the whole story. The longer call forms add multiple
targets, pagination, and a line range: grep_skipping, grep_with, and
grep_with_streaming (the streaming variant pushes each match to a callback as
it is found).
Output bounds
Grep output is windowed so the model’s context stays bounded:
| Bound | Value | Meaning |
|---|---|---|
| Files per call | 20 (directory/multi-file scopes) | At most 20 distinct files are surfaced. Page the next window with skip. |
| Matches per file | 20 multi-file, 200 single-file | Keeps a hot file from crowding out diverse hits. |
| Line length | 200 chars | Longer lines are cut with a ... suffix and flagged via truncated — read the file for the full line. |
When more files matched than the window, file_limit_reached: true and a
note tell you to call again with the same pattern/path plus skip=<N> for
the next page. When a single file exceeded its match cap,
per_file_limit_reached: true.
max_count changes the contract. A caller that sets max_count is not
paginating: the cap bounds the output directly, and the file window is not
applied (so there is no skip to follow).
Options
| Option | Default | Effect |
|---|---|---|
paths |
— | Multiple targets (files or directories) in one call; overrides path. |
line_range |
— | "start-end", 1-based inclusive. Requires single-file targets. |
glob / name_glob |
— | Restrict to files whose names match a glob (e.g. "*.rs"). |
file_type / language |
— | Restrict to a language/type (e.g. "rust", "py"). |
ignore_case |
false |
Case-insensitive matching. |
max_count |
— | Total match cap; disables file-window paging. |
skip |
0 |
Files to skip before collecting — paginate past the window. Ignored for single-file scopes and whenever max_count is set. |
context_before / context_after |
none | Context lines around each match. |
hidden |
true |
Include .-prefixed files. |
gitignore |
true |
Respect .gitignore. |
timeout_ms |
5000 |
Partials with timed_out: true on expiry. |
Patterns containing a newline (literal or \n escape) automatically enable
multiline matching — the engine’s line mode would otherwise silently return
zero matches for such patterns.
Reading the result
matches— the matched lines (path,line_number,line,truncated, context). Paths are absolute for single-file scopes and root-relative (or common-ancestor-relative for multi-target calls) for directory searches.total_matches— the deduplicated per-line union across targets before the per-file caps and window trim (itself bounded by the engine’s fetch caps), so it is a reliable lower bound even when matches were trimmed. Withline_rangeit is the count of in-range matches.files_searched/files_with_matches— always reported.useless: true+note: "No matches found"— zero matches, no timeout. Adjust the pattern or scope instead of retrying blindly. If you were already past the last page, the note says so (No more results (...)).timed_out: true— partial results; an empty timed-out result is an incomplete scan, not proof of absence.
Editing matches directly
Every shown file within the anchor window (the first 20 in encounter order)
gets an entry in the files array:
path: src/a.rs
file_hash: AE23
header: ¶/abs/project/src/a.rs#AE23
These are real, whole-file hashline tags: minting one reads the whole file,
records the snapshot in the session rollback history, and records the surfaced
lines. To edit a match, pass the anchor’s absolute path (the header’s
¶path#TAG, so the path is absolute and round-trips through fs_edit’s path
validation) and the file_hash to fs_edit directly — no re-read is needed to
obtain the current hash:
let out = find.grep("fn double", "src")?;
let f = &out.files[0]; // header "¶/abs/project/src/lib.rs#0C54"
let abs_path = f.header
.trim_start_matches('¶') // strip the `¶` file marker (multi-byte)
.split('#').next().unwrap(); // "/abs/project/src/lib.rs"
fs.edit(serde_json::json!({
"targets": [{
"path": abs_path,
"file_hash": f.file_hash,
"ops": "replace 1..3:\n+fn triple(x: i32) -> i32 { x * 3 }",
}]
}))?;
Files beyond the anchor window surface plain, headerless matches — there are no tags for them in this call.
Errors and edge cases
| Situation | What happens |
|---|---|
| Invalid regex | Hard Err from the regex engine. |
| No target provided | Hard Err (missing 'path' or 'paths' / no search targets provided). |
line_range with a directory target |
Hard Err (line_range requires single-file targets...). |
Malformed line_range |
Hard Err (line_range must be "start-end"...). |
| Target denied by the guards | Hard Err from the path guard. |
| Timeout | Partial matches with timed_out: true, never a blind error. |
Example
A complete runnable example is provided at
examples/find/grep.rs.
It builds a scratch project, searches it for a function name, then uses the
returned hashline anchor to edit one of the matched files directly.
Next: rollback — the previous page’s sibling module, or back to the module overview.