Files
oh-my-pi/docs/tools/edit.md
T
2026-05-11 00:38:35 +02:00

206 lines
14 KiB
Markdown

# edit
> Applies source edits; default mode is the hashline patch language consumed from a single `input` string.
## Source
- Entry: `packages/coding-agent/src/edit/index.ts`
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/hashline.md`
- Key collaborators:
- `packages/coding-agent/src/utils/edit-mode.ts` — selects active edit mode
- `packages/coding-agent/src/hashline/grammar.lark` — custom-tool grammar for hashline mode
- `packages/coding-agent/src/hashline/input.ts` — splits `@PATH` sections
- `packages/coding-agent/src/hashline/parser.ts` — parses ops and payload lines
- `packages/coding-agent/src/hashline/apply.ts` — validates anchors and applies edits
- `packages/coding-agent/src/hashline/anchors.ts` — stale-anchor mismatch formatting
- `packages/coding-agent/src/hashline/recovery.ts` — cache-based stale-anchor recovery
- `packages/coding-agent/src/hashline/hash.ts` — computes `LINEhh|` anchors shared with `read`/`search`
- `packages/coding-agent/src/edit/file-read-cache.ts` — per-session read snapshot cache
- `packages/coding-agent/src/tools/read.ts` — emits anchored lines and records read snapshots
- `packages/coding-agent/src/tools/search.ts` — records sparse snapshots from matches/context
- `packages/coding-agent/src/tools/fs-cache-invalidation.ts` — invalidates FS scan caches after writes
- `packages/coding-agent/src/edit/streaming.ts` — computes in-flight diff previews for the TUI
## Inputs
### Hashline mode (default)
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `input` | `string` | Yes | One or more edit sections. First non-blank line must be `@PATH` unless the caller supplies the legacy fallback `path` outside the model schema and the body already looks like hashline ops (`packages/coding-agent/src/hashline/input.ts`). Optional `*** Begin Patch` / `*** End Patch` envelope is ignored if present. |
Patch language inside `input`:
- Section header: `@PATH`
- Insert after: `+ ANCHOR`
- Insert before: `< ANCHOR`
- Delete range: `- A..B`
- Replace range: `= A..B`
- Payload line: `~TEXT` by default; separator is `HL_EDIT_SEP` and can be overridden once at process start by `PI_HL_SEP` (`packages/coding-agent/src/hashline/hash.ts`)
- Special anchors: `BOF`, `EOF`
- Anchor token: `<line><2-char-hash>`, for example `41th`
Anchors come from `read`/`search` output. `read` formats lines as `LINEhh|TEXT` via `formatHashLine` / `formatHashLines` in `packages/coding-agent/src/hashline/hash.ts`; copy only the token left of `|` into op lines.
Other edit modes exist (`replace`, `patch`, `vim`, `apply_patch`) and are selected outside the tool payload by `resolveEditMode()` in `packages/coding-agent/src/utils/edit-mode.ts`. Their schemas are different; this document covers the default hashline mode.
## Outputs
- Single-shot tool result; hashline mode does not use a `resolve` preview/apply handshake.
- `content` contains one text block per call. For a successful single-file edit it is either:
- `<path>:` plus a compact diff preview from `packages/coding-agent/src/hashline/diff-preview.ts`, or
- `Updated <path>` / `Created <path>` when no compact preview text is emitted.
- Parse or recovery warnings are appended as:
```text
Warnings:
...
```
- `details` is `EditToolDetails` from `packages/coding-agent/src/edit/renderer.ts`:
- `diff`: unified diff string
- `firstChangedLine`: first changed post-edit line
- `diagnostics`: LSP/format result if available
- `op`: `"create"` or `"update"` for hashline mode
- `meta`: output metadata
- `perFileResults`: present for multi-section input
- Multi-section input returns one aggregated result with combined text and per-file details.
- While the model is still typing arguments, the TUI can compute a diff preview with `packages/coding-agent/src/edit/streaming.ts`; that preview is not a deferred action and does not block execution.
## Flow
1. `EditTool.execute()` in `packages/coding-agent/src/edit/index.ts` resolves the active mode. Default is `hashline`; `customFormat` exposes `packages/coding-agent/src/hashline/grammar.lark` with `$HFMT$` / `$HSEP$` placeholders filled from `packages/coding-agent/src/hashline/hash.ts`.
2. `executeHashlineSingle()` in `packages/coding-agent/src/hashline/execute.ts` splits the raw `input` into `@PATH` sections with `splitHashlineInputs()`.
3. If multiple sections target the same path, `mergeSamePathSections()` concatenates them before execution so every op still refers to the original file snapshot.
4. Multi-section calls run a preflight pass (`preflightHashlineSection()`): parse ops, enforce plan-mode write rules, load the current file, reject anchor-scoped edits against missing files, reject auto-generated files, apply edits in memory, and fail if the result is a no-op. This prevents partial batches.
5. `parseHashlineWithWarnings()` in `packages/coding-agent/src/hashline/parser.ts` tokenizes the diff body:
- ignores blank lines and optional `*** Begin Patch`
- stops at `*** End Patch`
- stops at `*** Abort` and emits `ABORT_WARNING`
- turns `+` / `<` payload runs into one `insert` edit per payload line
- turns `- A..B` into one `delete` edit per line in the range
- turns `= A..B` into inserts before `A`, then deletes for `A..B`; no payload means replace with a single empty line
6. `applyHashlineEdits()` in `packages/coding-agent/src/hashline/apply.ts` validates every referenced anchor before mutating anything. Each anchor hash is recomputed from current file content with `computeLineHash()`.
7. If any anchor hash differs, `applyHashlineEdits()` throws `HashlineMismatchError`. `execute.ts` catches only that class and calls `tryRecoverHashlineWithCache()`.
8. Recovery replays the edits against the most recent cached read/search snapshot for that path (`packages/coding-agent/src/edit/file-read-cache.ts`), then 3-way merges the result onto current disk content using `Diff.applyPatch(..., { fuzzFactor: 3 })` in `packages/coding-agent/src/hashline/recovery.ts`. On success the edit proceeds with a warning; on failure the original mismatch error is re-thrown.
9. Before splicing lines, `absorbReplacementBoundaryDuplicates()` normalizes some malformed-but-recoverable ranges:
- duplicate prefix/suffix lines adjacent to a replacement can be absorbed by widening the delete range
- pure inserts can auto-drop duplicated leading/trailing payload lines when `edit.hashlineAutoDropPureInsertDuplicates` is enabled
- all such fixes append warnings
10. `after_anchor` inserts are normalized to `before_anchor` of the next line, or `EOF` if the anchor was the last line.
11. Anchor-targeted edits are bucketed by target line and applied bottom-up so earlier splices do not invalidate later original line numbers. `BOF` and `EOF` inserts are applied after that.
12. The edited text is restored to the original BOM and line ending style with helpers from `packages/coding-agent/src/edit/normalize.ts` and persisted via `serializeEditFileText()` in `packages/coding-agent/src/edit/read-file.ts`.
13. The writethrough callback from `createLspWritethrough()` may format the file and fetch diagnostics. Late diagnostics are queued back into session state as a hidden deferred message by `EditTool.#injectLateDiagnostics()` in `packages/coding-agent/src/edit/index.ts`.
14. `invalidateFsScanAfterWrite()` calls `invalidateFsScanCache(path)` so filesystem-backed tools do not serve stale scan results.
15. The session file-read cache is refreshed with the post-edit file text via `recordContiguous()`, making the just-written content the new recovery base for subsequent stale-anchor merges.
16. The final response is built from a unified diff (`generateDiffString()`), a compact preview, and any accumulated warnings.
## Modes / Variants
- `hashline` — default mode; line-anchored patch language described here (`packages/coding-agent/src/utils/edit-mode.ts`).
- `replace` — exact/fuzzy old/new text replacement (`packages/coding-agent/src/edit/modes/replace.ts`).
- `patch` — structured JSON diff-hunk mode (`packages/coding-agent/src/edit/modes/patch.ts`).
- `apply_patch` — freeform Codex-style `*** Begin Patch` envelope, internally expanded into patch-mode entries (`packages/coding-agent/src/edit/modes/apply-patch.ts`).
- `vim` — persistent modal editing buffer (`packages/coding-agent/src/tools/vim.ts`).
Hashline op examples:
```text
@src/a.ts
+ 4fb
~const added = true;
```
```text
@src/a.ts
< 4fb
~const addedBefore = true;
```
```text
@src/a.ts
- 4fb..6qx
```
```text
@src/a.ts
= 4fb..5dm
~const clean = (name || DEF).trim();
~return clean.length === 0 ? DEF : clean.toUpperCase();
```
BOF/EOF examples:
```text
@src/a.ts
+ BOF
~const HEADER = true;
```
```text
@src/a.ts
+ EOF
~export const done = true;
```
## Side Effects
- Filesystem
- Reads target files with `readEditFileText()`.
- Writes full updated file contents with `serializeEditFileText()`.
- Preserves BOM and original line-ending style.
- Subprocesses / native bindings
- `createLspWritethrough()` may trigger formatter / diagnostics work through the LSP subsystem.
- `invalidateFsScanAfterWrite()` calls native `invalidateFsScanCache()` from `@oh-my-pi/pi-natives`.
- Session state
- Reads and updates the per-session `FileReadCache` used for stale-anchor recovery.
- Stores pending deferred-diagnostics abort controllers per path inside `EditTool`.
- Queues late diagnostics back into the session transcript as a hidden custom message.
- Background work / cancellation
- A new edit to the same path aborts the prior deferred diagnostics fetch for that path (`packages/coding-agent/src/edit/index.ts`).
- The tool itself is marked `nonAbortable = true` and `concurrency = "exclusive"` in `packages/coding-agent/src/edit/index.ts`.
## Limits & Caps
- Default mode is `hashline` (`DEFAULT_EDIT_MODE`) in `packages/coding-agent/src/utils/edit-mode.ts`.
- Anchor hashes are always 2 lowercase letters from a stable 647-entry bigram table (`HL_BIGRAMS_COUNT`) in `packages/coding-agent/src/hashline/hash.ts`.
- The visible mismatch report shows 2 lines of context on each side (`MISMATCH_CONTEXT`) in `packages/coding-agent/src/hashline/constants.ts`.
- Stale-anchor recovery uses `fuzzFactor: 3` (`HASHLINE_RECOVERY_FUZZ_FACTOR`) in `packages/coding-agent/src/hashline/recovery.ts`.
- The per-session read cache keeps at most 30 paths (`MAX_PATHS_PER_SESSION`) in `packages/coding-agent/src/edit/file-read-cache.ts`.
- Hashline streaming chunk defaults are 200 lines or 64 KiB per chunk (`packages/coding-agent/src/hashline/types.ts`, consumed by `packages/coding-agent/src/hashline/stream.ts`).
- `HL_EDIT_SEP` defaults to `~`; `HL_BODY_SEP` is always `|` (`packages/coding-agent/src/hashline/hash.ts`).
## Errors
- Missing section header:
- `input must begin with "@PATH" on the first non-blank line; got: ... Example: "@src/foo.ts" then edit ops.`
- Empty header:
- `Input header "@" is empty; provide a file path.`
- Bad anchor token:
- `line N: expected a full anchor such as "119sr"; got "...".`
- Bad range syntax:
- `line N: explicit ranges are required for delete/replace...`
- `line N: range must include exactly two full anchors separated by "..".`
- `line N: range A..B ends before it starts.`
- `line N: range A..B uses two different hashes for the same line.`
- Missing payload for `+` / `<`:
- `line N: + and < operations require at least one ~TEXT payload line.`
- Stray payload line:
- `line N: payload line has no preceding +, <, or = operation.`
- Unknown op:
- `line N: unrecognized op. Use < ANCHOR..., + ANCHOR..., - A..B..., = A..B...`
- Missing file for anchor-scoped edits:
- `File not found: <path>`
- Out-of-range anchor:
- `Line N does not exist (file has M lines)`
- Stale anchors throw `HashlineMismatchError`. The error message contains re-read guidance and reprints nearby current file lines as `LINEhh|TEXT`; mismatched lines are marked `*`. `displayMessage` renders the same information in a code-frame style.
- No-op edit:
- `Edits to <path> resulted in no changes being made.`
- Recovery failure is silent internally: if cache-based merge cannot prove a valid result, the original mismatch error is surfaced unchanged.
## Notes
- `read` and `search` are the authoritative source of anchors. The edit parser does not want the trailing `|TEXT`; copy only the `LINEhh` token.
- Multi-op patches are parsed against the original file snapshot. Do not renumber later anchors after earlier ops; `applyHashlineEdits()` buckets and applies them bottom-up.
- `= A..B` is not a primitive replace in the parser. It expands to inserts before `A` plus deletes for `A..B`, which is why stale-anchor checking still happens on the original range lines.
- Interior lines of a multi-line range use hash `**` (`RANGE_INTERIOR_HASH`) and are not individually verified; only the first and last anchor hashes are checked.
- `computeLineHash()` trims trailing whitespace before hashing. Anchors survive line-ending changes and trailing-space-only changes, but not substantive line edits.
- For punctuation-only lines, the hash mixes in the line number; identical `}` lines on different lines intentionally get different anchors.
- `splitHashlineInputs()` normalizes absolute `@PATH` headers back to a cwd-relative path when the file is inside the current working tree.
- Optional `*** Begin Patch` / `*** End Patch` markers are accepted in hashline mode, but the file sections are still `@PATH`-based, not Codex `*** Update File:` hunks.
- `*** Abort` terminates parsing early and returns `ABORT_WARNING`; ops parsed before the marker still apply.
- File-read cache invalidation is conflict-based, not write-through invalidation. If `read` later records content for a line that disagrees with the cached snapshot, the entire snapshot for that path is replaced with the newly observed lines (`packages/coding-agent/src/edit/file-read-cache.ts`).
- There is no resolve-style apply/discard phase for hashline edits. The only preview path is the transient TUI diff preview in `packages/coding-agent/src/edit/streaming.ts`.