edit — the hashline replace engine
edit applies targeted changes to one or more files. It is the surgical
alternative to write: instead of replacing a whole file, it changes specific
lines or blocks, anchored by the file’s content hash for safety.
Fs::edit(&self, args: serde_json::Value) -> Result<Vec<EditResult>, String>
Fs::edit is a dispatcher: depending on the configured
EditEngine, the args it receives select the
engine to run. This page covers the hashline replace engine (the targets
argument, or the engine forced by Fs::only_replace()). The structural engine
is documented on its own page, ast_edit.
The idea: hash-anchored edits
Before any edit is applied, the engine re-hashes the file’s current content
and compares it with the file_hash you passed in:
- Hash matches → the file is exactly what you read; the edit is safe.
- Hash differs → the file changed since you read it. The engine attempts an automatic 3-way merge recovery (your edit against the live content), and only applies if the merge is unambiguous.
This is what prevents editing a stale view of a file. It is also why the
file_hash must be copied verbatim from the ¶path#TAG header of a read or
search result — never re-typed.
Order carries intention. Targets are applied in the order you list them; an
edit may build on the result of a previous one. When a target fails, the batch
stops: earlier targets stay applied (returned with fresh hash tags), and later
targets are skipped as a consequence of the failure — see
EditBatchError.
The ops mini-language
Each EditTarget
carries an ops string: one operation per line. The operations are:
| Operation | Syntax | Effect |
|---|---|---|
| Replace | replace N..M: followed by +-prefixed lines |
Replace lines N through M with the new content. |
| Replace one line | replace N: + +line |
Replace just line N (lenient form of N..N). |
| Delete | delete N or delete N..M |
Delete line N, or lines N through M. |
| Insert before | insert before N: + +-prefixed lines |
Insert lines before line N. |
| Insert after | insert after N: + +-prefixed lines |
Insert lines after line N. |
| Insert at head | insert head: + lines |
Insert at the start of the file. |
| Insert at tail | insert tail: + lines |
Insert at the end of the file. |
| Replace block | replace block N: + lines |
Resolve the tree-sitter block starting at line N and replace it. |
Each payload line must begin with + (the payload sigil). Multi-line payloads
repeat the + prefix on every line. The parser is deliberately lenient about
separators (.., -, …, or spaces between range endpoints are all accepted;
a trailing : on replace/insert headers is optional — for delete it is an
error) but a line that starts with + when no body is expected is an error.
A complete ops string for a two-part edit:
replace 5..7:
+fn hello() {
+ println!("hi");
+}
delete 10..12
insert after 15:
+// new comment
Line numbers refer to the file as it was when you read it — the hashline
numbers in the N| text output. The engine maps them through the content hash
anchor, so intervening edits to the file are detected rather than silently
mis-applied.
Fs::edit in Replace mode
When the Fs is forced to the replace engine (only_replace()), edit expects
exactly one argument: a non-empty targets array.
let args = serde_json::json!({
"targets": [{
"path": "src/main.rs",
"file_hash": "3C4D", // copied from ¶src/main.rs#3C4D
"ops": "replace 1..3:\n+fn main() {\n+ println!(\"hi\");\n+}"
}]
});
let results = fs.edit(args).await?;
If targets is missing, edit returns an error explaining the expected shape.
In Auto mode
The default EditEngine::Auto inspects the arguments and routes accordingly:
a non-empty targets array runs the replace engine; an ast object runs the
AST engine. Providing both or neither is an error, and filling targets with
AST-style metavariables ($NAME) is detected and corrected with a prompt
telling you the AST schema belongs in ast. See
the dispatcher for the full picture.
What you get back
Each applied target yields an EditResult: the fresh
hash tag (your new anchor — keep editing without re-reading), the 1-based
first_changed_line, any warnings, and a unified diff of the change (only
when something actually changed). A target whose edit produced no change returns
a result with diff: None.
On batch failure, Err carries an EditBatchError
describing which target failed, which were applied before it, and which were
skipped after.
Example
A complete runnable example is provided at
examples/fs/edit.rs. It
writes a small source file, reads it back to capture its hashline tag, then
applies a multi-operation edit and prints the resulting diff.