read — reading files, symbols, and line ranges
read displays the contents of one or more files for the model. It is the
foundation of the whole fs module: everything you do — editing,
rolling back — begins by reading a file and copying the hashline tag and line
numbers from its output.
Fs::read(&self, targets: Vec<Target>) -> Vec<ReadResult>
It is the only file-system operation that never fails as a whole: errors are
reported per-target inside each ReadResult.warnings, and the rest of the
batch still runs. A missing file, an unresolvable block, or an invalid range
produces a warning, not an aborted batch.
This page walks through the four ways to read, the token-saving behaviors, and what the output looks like.
The four kinds of read
A Target selects how much of a file to
show. Remember the precedence rules:
symbol— a definition by name, checked first, wins over everything.line_range— exact lines, plain slice, checked beforeline.line— the syntactic block containing a line.- neither — the whole file.
1. Whole-file read
The simplest read: just a path, nothing else. The entire file is returned with
every line numbered.
let results = fs
.read(vec![Target { path: "src/main.rs".into(), line: None, symbol: None, line_range: None }])
.await;
Output (abridged):
¶src/main.rs#3C4D
1| use clap::Parser;
2| use serde_json::Value;
3|
...
The ¶src/main.rs#3C4D header is your anchor: copy the tag 3C4D when you edit
the file later. Whole-file reads record every line as “seen”, so later edits
anchoring any of those lines are safe.
2. Read by line — a syntactic block
Set line to a 1-based line number. The reader does not return just that
line: it uses tree-sitter to resolve the syntactic block containing it — the
enclosing function, struct, impl, enum, trait, or module — and returns the whole
block. This is the right tool when you want, say, “the function around line 40”.
Target { path: "src/main.rs".into(), line: Some(40), symbol: None, line_range: None }
If the line does not begin a valid block, or the file has no grammar for the line to resolve against, the read falls back to the whole file and reports a warning explaining the likely causes.
3. Read by symbol — a definition by name
Set symbol to the name of a function, struct, class, or variable. The reader
searches the file for the definition and returns its block. When path is a
directory, the whole tree is walked recursively and every supported source
file is searched:
// Search src/greeter.rs for a definition named `greet`.
Target { path: "src/greeter.rs".into(), line: None, symbol: Some("greet".into()), line_range: None }
// Search every source file under src/ for a definition named `main`.
Target { path: "src".into(), line: None, symbol: Some("main".into()), line_range: None }
A directory read without a symbol is an error — the tool refuses to dump an
entire tree. If the symbol is not found anywhere, one result is returned with a
warning telling you to verify the spelling.
4. Read by line_range — exact lines, no AST
Sometimes you already know precisely which lines you need — for example, a block
was elided in a previous read and the footer told you to re-read lines 30-80.
line_range gives you a plain slice with no AST resolution:
// Lines 50 through 100.
Target { path: "src/main.rs".into(), line: None, symbol: None, line_range: Some("50-100".into()) }
// Two disjoint ranges in one read.
Target { path: "src/main.rs".into(), line: None, symbol: None, line_range: Some("10-20,200-220".into()) }
Ranges are 1-based and inclusive. A line_range past the end of the file is
clamped, with a warning if it starts beyond EOF.
What the output looks like
Every result is one ReadResult: a path, the content
hash file_hash, the hashline header, the numbered content, and optional
warnings. The content is composed of three parts:
- The hashline header —
¶path#TAG, the anchor for edits. - Numbered lines —
N| text, 1-based, the numbers you reference in edits. - Content notices — appended after the body, informing the model about elision and how to continue. These are not warnings; warnings are reserved for delivery problems (missing file, bad range, symbol not found).
Token-saving behaviors
read is designed to keep the model’s context lean. When you read less than a
whole file, three token-saving behaviors kick in automatically:
-
Elided blocks. A block (from
line/symbolresolution) larger than 24 lines is shown as its head (3 lines), a…marker, and its tail (2 lines), with a footer naming the exactline_rangeto re-read for the elided body:[…78 lines elided; re-read with line_range "31-108"] -
Column truncation. Lines longer than 200 characters are cut with a
...suffix and a notice telling you to useline_rangefor the full content. Truncation is flagged in the output so you know the line is incomplete. -
Exact-range reads. Reading
line_rangereturns only the requested lines, with a footer reporting how many lines remain and how to continue:[240 more lines in file; continue with line_range "61-300"]Between multiple ranges, gaps are marked with a summary line (
[…50 lines between ranges elided]).
This “read only what you need” loop is the intended usage pattern: read a block, notice it is elided, re-read the exact range, edit precisely.
Seen-line tracking
Every line the read surfaces is recorded against the file’s hash tag in the session’s rollback history. This matters for safety: if an edit later anchors a line the model never actually saw — for example an elided interior — the recovery path can warn instead of silently applying an edit against an unknown body.
Errors and edge cases
Because errors are per-target, there is no top-level error type for read.
Instead, look at warnings:
| Situation | What happens |
|---|---|
| File does not exist / cannot be read | ReadResult with a warning; empty content. |
path is a directory without a symbol |
A result explaining that a symbol is required. |
| Symbol not found | A result with a hint to verify spelling or use line. |
| No grammar for the language | A warning; use line targeting or read the whole file. |
Invalid line_range syntax |
A warning plus the whole file fallback. |
line does not start a valid block |
A warning plus the whole file fallback. |
| Path denied by the guards | A permission denied warning; the file is not read. |
In short: read always tells you something, and warnings is where you look
when a result’s content is missing or surprising.
Example
A complete runnable example is provided at
examples/fs/read.rs. It
creates a scratch project, writes a small source file, and demonstrates all four
read modes — whole file, line block, symbol, and exact ranges.