From 012e0c90b9f947ef004e88b86fb79051c99d2b8b Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 22 Mar 2026 21:14:31 +0100 Subject: [PATCH] feat(coding-agent): renamed hashline operation types for clarity - Renamed hashline operation types for clarity: append->append_at, prepend->prepend_at, append_eof->append_file, prepend_bof->prepend_file. - Updated all operation type references in patch implementation, tests, and documentation to reflect new naming convention. - Restructured hashline tool documentation with hierarchical sections and simplified examples for improved clarity. - Consolidated validation rules and added explicit warning about invalid anchors and operation/field combinations. --- packages/coding-agent/CHANGELOG.md | 2 +- packages/coding-agent/src/patch/hashline.ts | 40 ++--- packages/coding-agent/src/patch/index.ts | 44 +++--- .../src/prompts/tools/hashline.md | 148 ++++++------------ .../coding-agent/test/core/hashline.test.ts | 36 ++--- 5 files changed, 106 insertions(+), 164 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1981256bd..bad9eab69 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,9 +1,9 @@ # Changelog ## [Unreleased] - ### Breaking Changes +- Renamed hashline edit operation types: `append` → `append_at`, `prepend` → `prepend_at`, `append_eof` → `append_file`, `prepend_bof` → `prepend_file` - Changed hashline edit operation types from `replace` (with optional `end`) to explicit `replace_line` and `replace_range` operations - Added required `append_eof` and `prepend_bof` operations for file-level edits; `append` and `prepend` now require an anchor position - Made `pos` parameter required for `replace_line`, `append`, and `prepend` operations; `append_eof` and `prepend_bof` no longer accept anchors diff --git a/packages/coding-agent/src/patch/hashline.ts b/packages/coding-agent/src/patch/hashline.ts index 89e1df7e7..e4c2b6df5 100644 --- a/packages/coding-agent/src/patch/hashline.ts +++ b/packages/coding-agent/src/patch/hashline.ts @@ -18,10 +18,10 @@ export type Anchor = { line: number; hash: string }; export type HashlineEdit = | { op: "replace_line"; pos: Anchor; lines: string[] } | { op: "replace_range"; pos: Anchor; end: Anchor; lines: string[] } - | { op: "append"; pos: Anchor; lines: string[] } - | { op: "prepend"; pos: Anchor; lines: string[] } - | { op: "append_eof"; lines: string[] } - | { op: "prepend_bof"; lines: string[] }; + | { op: "append_at"; pos: Anchor; lines: string[] } + | { op: "prepend_at"; pos: Anchor; lines: string[] } + | { op: "append_file"; lines: string[] } + | { op: "prepend_file"; lines: string[] }; const NIBBLE_STR = "ZPMQVRWSNKTXJBYH"; @@ -517,16 +517,16 @@ export function applyHashlineEdits( } break; } - case "append": - case "prepend": { + case "append_at": + case "prepend_at": { if (!validateRef(edit.pos)) continue; if (edit.lines.length === 0) { edit.lines = [""]; // insert an empty line } break; } - case "append_eof": - case "prepend_bof": { + case "append_file": + case "prepend_file": { if (edit.lines.length === 0) { edit.lines = [""]; // insert an empty line } @@ -552,16 +552,16 @@ export function applyHashlineEdits( case "replace_range": lineKey = `r:${edit.pos.line}:${edit.end.line}`; break; - case "append": + case "append_at": lineKey = `i:${edit.pos.line}`; break; - case "prepend": + case "prepend_at": lineKey = `ib:${edit.pos.line}`; break; - case "append_eof": + case "append_file": lineKey = "ieof"; break; - case "prepend_bof": + case "prepend_file": lineKey = "ibef"; break; } @@ -591,19 +591,19 @@ export function applyHashlineEdits( sortLine = edit.end.line; precedence = 0; break; - case "append": + case "append_at": sortLine = edit.pos.line; precedence = 1; break; - case "prepend": + case "prepend_at": sortLine = edit.pos.line; precedence = 2; break; - case "append_eof": + case "append_file": sortLine = fileLines.length + 1; precedence = 1; break; - case "prepend_bof": + case "prepend_file": sortLine = 0; precedence = 2; break; @@ -637,7 +637,7 @@ export function applyHashlineEdits( trackFirstChanged(edit.pos.line); break; } - case "append": { + case "append_at": { const inserted = edit.lines; if (inserted.length === 0) { noopEdits.push({ @@ -651,7 +651,7 @@ export function applyHashlineEdits( trackFirstChanged(edit.pos.line + 1); break; } - case "prepend": { + case "prepend_at": { const inserted = edit.lines; if (inserted.length === 0) { noopEdits.push({ @@ -665,7 +665,7 @@ export function applyHashlineEdits( trackFirstChanged(edit.pos.line); break; } - case "append_eof": { + case "append_file": { const inserted = edit.lines; if (inserted.length === 0) { noopEdits.push({ editIndex: idx, loc: "EOF", current: "" }); @@ -680,7 +680,7 @@ export function applyHashlineEdits( } break; } - case "prepend_bof": { + case "prepend_file": { const inserted = edit.lines; if (inserted.length === 0) { noopEdits.push({ editIndex: idx, loc: "BOF", current: "" }); diff --git a/packages/coding-agent/src/patch/index.ts b/packages/coding-agent/src/patch/index.ts index 38651e2a8..79ee06338 100644 --- a/packages/coding-agent/src/patch/index.ts +++ b/packages/coding-agent/src/patch/index.ts @@ -176,7 +176,7 @@ export function hashlineParseText(edit: string[] | string | null): string[] { const hashlineEditSchema = Type.Object( { - op: StringEnum(["replace_line", "replace_range", "append", "prepend", "append_eof", "prepend_bof"]), + op: StringEnum(["replace_line", "replace_range", "append_at", "prepend_at", "append_file", "prepend_file"]), pos: Type.Optional(Type.String({ description: "anchor" })), end: Type.Optional(Type.String({ description: "limit position" })), lines: Type.Union([ @@ -211,10 +211,10 @@ export type HashlineParams = Static; * Resilient: as long as at least one anchor exists, we execute. * - replace_line + tag → single-line replace * - replace_range + tag + end → range replace - * - append + tag or end → append after that anchor - * - prepend + tag or end → prepend before that anchor - * - append_eof → file-level append (no anchors needed) - * - prepend_bof → file-level prepend (no anchors needed) + * - append_at + tag or end → append after that anchor + * - prepend_at + tag or end → prepend before that anchor + * - append_file → file-level append (no anchors needed) + * - prepend_file → file-level prepend (no anchors needed) * * Unknown ops default to replace_line/replace_range based on available anchors. */ @@ -237,24 +237,24 @@ function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] { result.push({ op: "replace_range", pos: tag, end, lines }); break; } - case "append": { + case "append_at": { const anchor = tag ?? end; - if (!anchor) throw new Error("append requires an anchor (pos)."); - result.push({ op: "append", pos: anchor, lines }); + if (!anchor) throw new Error("append_at requires an anchor (pos)."); + result.push({ op: "append_at", pos: anchor, lines }); break; } - case "prepend": { + case "prepend_at": { const anchor = end ?? tag; - if (!anchor) throw new Error("prepend requires an anchor (pos)."); - result.push({ op: "prepend", pos: anchor, lines }); + if (!anchor) throw new Error("prepend_at requires an anchor (pos)."); + result.push({ op: "prepend_at", pos: anchor, lines }); break; } - case "append_eof": { - result.push({ op: "append_eof", lines }); + case "append_file": { + result.push({ op: "append_file", lines }); break; } - case "prepend_bof": { - result.push({ op: "prepend_bof", lines }); + case "prepend_file": { + result.push({ op: "prepend_file", lines }); break; } default: { @@ -575,8 +575,8 @@ export class EditTool implements AgentTool { const lines: string[] = []; for (const edit of edits) { // For file creation, only anchorless appends/prepends are valid - if (edit.op === "append_eof" || edit.op === "prepend_bof") { - if (edit.op === "prepend_bof") { + if (edit.op === "append_file" || edit.op === "prepend_file") { + if (edit.op === "prepend_file") { lines.unshift(...hashlineParseText(edit.lines)); } else { lines.push(...hashlineParseText(edit.lines)); @@ -605,7 +605,7 @@ export class EditTool implements AgentTool { const originalNormalized = normalizeToLF(text); let normalizedText = originalNormalized; - // Apply anchor-based edits first (replace, append, prepend) + // Apply anchor-based edits first (replace, append_at, prepend_at) const anchorResult = applyHashlineEdits(normalizedText, anchorEdits); normalizedText = anchorResult.lines; @@ -641,12 +641,12 @@ export class EditTool implements AgentTool { case "replace_range": refs.push(edit.end, edit.pos); break; - case "append": - case "prepend": + case "append_at": + case "prepend_at": refs.push(edit.pos); break; - case "append_eof": - case "prepend_bof": + case "append_file": + case "prepend_file": break; } diff --git a/packages/coding-agent/src/prompts/tools/hashline.md b/packages/coding-agent/src/prompts/tools/hashline.md index ce3342160..fcbc3f233 100644 --- a/packages/coding-agent/src/prompts/tools/hashline.md +++ b/packages/coding-agent/src/prompts/tools/hashline.md @@ -1,27 +1,36 @@ -Applies precise, surgical file edits by referencing `LINE#ID` tags from `read` output. Each tag uniquely identifies a line, so edits remain stable even when lines shift. +Applies precise file edits using `LINE#ID` anchors from `read` output. -Read the file first to get fresh tags. Submit one `edit` call per file with all operations batched — tags shift after each edit, so multiple calls require re-reading between them. +Read the file first. Copy anchors exactly from the latest `read` output. In one `edit` call, batch all edits for one file. After any successful edit, re-read before editing that file again. + +This matters: your output is checked against the real file state. Invalid anchors, invalid op/field combinations, duplicated boundary lines, or semantically equivalent rewrites will fail. -**`path`** — the path to the file to edit. -**`move`** — if set, move the file to the given path. -**`delete`** — if true, delete the file. +**Top level** +- `path` — file path +- `move` — optional rename target +- `delete` — optional whole-file delete +- `edits` — array of edit entries -**`edits[n].pos`** — the anchor line. Meaning depends on `op`: - - if `replace_line`: the line to rewrite - - if `replace_range`: first line of the range to rewrite - - if `prepend`: line to insert new lines **before** - - if `append`: line to insert new lines **after** - - Not used by `append_eof` or `prepend_bof`. -**`edits[n].end`** — only used by `replace_range`: the last line of the range (inclusive). -**`edits[n].lines`** — the replacement content: - - for `replace_line`/`replace_range`: the exact lines that will replace the target line(s) - - for `append`/`prepend`/`append_eof`/`prepend_bof`: the new lines to insert - - `[""]` — blank line - - `null` or `[]` — delete if `replace_line`/`replace_range` -- If `lines` contains content that already exists after `end`, those lines **will be duplicated** in the output. -- Keep `lines` to exactly what belongs inside the consumed range. -- Ops are applied bottom-up. Tags **MUST** be referenced from the most recent `read` output. +**Edit entry shape** +Each entry is: +- `op` — one of `replace_line`, `replace_range`, `append_at`, `prepend_at`, `append_file`, `prepend_file` +- `lines` — replacement/inserted content +- `pos` — required for `replace_line`, `replace_range`, `append_at`, `prepend_at` +- `end` — required only for `replace_range` + +**Meaning** +- `replace_line`: replace exactly one anchored line +- `replace_range`: replace inclusive `pos..end` +- `append_at`: insert after `pos` +- `prepend_at`: insert before `pos` +- `append_file`: insert at end of file +- `prepend_file`: insert at beginning of file + +**`lines`** +- Array of literal file lines is preferred +- `""` means a blank line +- `null` or `[]` deletes for `replace_line` / `replace_range` +- For insert ops, `lines` must contain only the new content @@ -47,8 +56,7 @@ All examples below reference the same file, `util.ts`: {{hlinefull 18 "}"}} ``` - -Change the timeout from `5000` to `30_000`: + ``` { path: "util.ts", @@ -61,19 +69,7 @@ Change the timeout from `5000` to `30_000`: ``` - -Single line — `lines: null` deletes entirely: -``` -{ - path: "util.ts", - edits: [{ - op: "replace_line", - pos: {{hlineref 1 "// @ts-ignore"}}, - lines: null - }] -} -``` -Range — remove the legacy block (lines 10–11): + ``` { path: "util.ts", @@ -87,10 +83,8 @@ Range — remove the legacy block (lines 10–11): ``` - -Replace the catch body with smarter error handling. Shape (a): `pos` is the first body line, `end` is the last body line. The catch header (line 14) and its closer (line 17) are outside the range and stay untouched. - -When changing body content, replace the **entire** body span — not just one line inside it. Patching one line leaves the rest of the body stale. + +Replace only the catch body. Do not target the shared boundary line `} catch (err) {`. ``` { path: "util.ts", @@ -107,60 +101,13 @@ When changing body content, replace the **entire** body span — not just one li ``` - -Simplify `beta()` to a one-liner. Shape (b): `pos`=header, `end`=closer, re-emit all in `lines`. - -Bad — `end` stops at the inner `\t}` on line 17, so the outer `}` on line 18 survives. Result: two consecutive `}` lines. + +When adding a sibling declaration, prefer `prepend_at` on the next declaration. ``` { path: "util.ts", edits: [{ - op: "replace_range", - pos: {{hlineref 9 "function beta() {"}}, - end: {{hlineref 17 "\t}"}}, - lines: [ - "function beta() {", - "\treturn parse(data);", - "}" - ] - }] -} -``` -Good — `end` includes the function's own `}` on line 18, so the old closer is consumed: -``` -{ - path: "util.ts", - edits: [{ - op: "replace_range", - pos: {{hlineref 9 "function beta() {"}}, - end: {{hlineref 18 "}"}}, - lines: [ - "function beta() {", - "\treturn parse(data);", - "}" - ] - }] -} -``` - - - -Do not anchor `replace_range` on a mixed boundary line such as `} catch (err) {`, `} else {`, `}),`, or `},{`. Those lines belong to two adjacent structures at once. - -Bad — if you need to change code on both sides of that line, replacing just the boundary span will usually leave one side's syntax behind. - -Good — choose one of two safe shapes instead: -- move inward and replace only body-owned lines -- expand outward and replace one whole owned block, consuming its real closer/separator too - - - -Add a `gamma()` function between `alpha()` and `beta()`. Use `prepend` on the next declaration — not `append` on the previous block's closing brace — so the anchor is a stable declaration boundary. -``` -{ - path: "util.ts", - edits: [{ - op: "prepend", + op: "prepend_at", pos: {{hlineref 9 "function beta() {"}}, lines: [ "function gamma() {", @@ -171,22 +118,17 @@ Add a `gamma()` function between `alpha()` and `beta()`. Use `prepend` on the ne }] } ``` -Use a trailing `""` to preserve the blank line between sibling declarations. -- You **MUST NOT** use this tool to reformat, reindent, or adjust whitespace — run the project's formatter instead. -- Every tag **MUST** be copied exactly from your most recent `read` output as `N#ID`. Stale or mistyped tags cause mismatches. -- Edit payload: `{ path, edits[] }`. Each entry: `op`, `lines`, plus `pos` and/or `end` depending on op. `replace_line`/`append`/`prepend` require `pos`. `replace_range` requires both `pos` and `end`. `append_eof`/`prepend_bof` require neither. No extra keys. -- For `append`/`prepend`/`append_eof`/`prepend_bof`, `lines` **MUST** contain only the newly introduced content. Do not re-emit surrounding content, or terminators that already exist. -- When changing existing code near a block tail or closing delimiter, default to `replace_range` over the owned span instead of inserting around the boundary. -- When adding a sibling declaration, default to `prepend` on the next sibling declaration instead of `append` on the previous block's closing brace. -- **Block boundaries travel together.** For a block `{ header / body / closer }`, there are exactly two valid replace shapes: (a) replace only the body — `pos`=first body line, `end`=last body line, leave the header and closer untouched; or (b) replace the whole block — `pos`=header, `end`=closer, re-emit all three in `lines`. Never split them: do not set `end` to the closer while omitting it from `lines` (deletes it), and do not emit the closer in `lines` without including it in `end` (duplicates it). This applies to every block terminator: `}`, `continue`, `break`, `return`, `throw`. -- **Never target shared boundary lines.** Do not use `replace_range` spans that start, end, or pivot on a line that closes one construct and opens/separates another, such as `},{`, `}),`, `} else {`, or `} catch (err) {`. Those lines are not owned by a single block. Move the range inward to body-only lines, or widen it to consume one whole owned construct including its true trailing delimiter. -- **`lines` must not extend past `end`.** `lines` replaces exactly `pos..end`. Content after `end` survives. If you include lines in `lines` that exist after `end`, they will appear twice. Either extend `end` to cover all lines you are re-emitting, or remove the extra lines from `lines`. -- `lines` entries **MUST** be literal file content with indentation copied exactly from the `read` output. If the file uses tabs, use a real tab character. -- After any successful `edit` call on a file, the next change to that same file **MUST** start with a fresh `read`. Do not chain a second `edit` call off stale mental state, even if the intended range is nearby. -- If you need a second change in the same local region, default to one wider `replace` over the whole owned block instead of a sequence of micro-edits on adjacent lines. Repeated small patches in a moving region are unstable. -- If a local region is already malformed or a prior patch partially landed, stop nibbling at it. Re-read the file and replace the full owned block from a stable boundary; for a small file, prefer rewriting the file over stacking more tiny repairs. +- Make the minimum exact edit. Do not rewrite nearby code unless the consumed range requires it. +- Use anchors exactly as `N#ID` from the latest `read` output. +- `replace_range` requires both `pos` and `end`. All other anchored ops require `pos` only. +- `append_file` and `prepend_file` do not take anchors. +- Replace exactly the owned span. If `lines` re-emits content beyond `end`, it will duplicate. +- Do not target shared boundary lines such as `} else {`, `} catch (…) {`, `}),`, or `},{`. +- For a block, either replace only the body or replace the whole block. Do not split block boundaries. +- `lines` must be literal file content with matching indentation. If the file uses tabs, use real tabs. +- Do not use this tool to reformat or clean up unrelated code. \ No newline at end of file diff --git a/packages/coding-agent/test/core/hashline.test.ts b/packages/coding-agent/test/core/hashline.test.ts index 1e6e61187..d1edbccd3 100644 --- a/packages/coding-agent/test/core/hashline.test.ts +++ b/packages/coding-agent/test/core/hashline.test.ts @@ -350,7 +350,7 @@ describe("applyHashlineEdits — delete", () => { describe("applyHashlineEdits — append", () => { it("inserts after a line", () => { const content = "aaa\nbbb\nccc"; - const edits: HashlineEdit[] = [{ op: "append", pos: makeTag(1, "aaa"), lines: ["NEW"] }]; + const edits: HashlineEdit[] = [{ op: "append_at", pos: makeTag(1, "aaa"), lines: ["NEW"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\nNEW\nbbb\nccc"); @@ -359,7 +359,7 @@ describe("applyHashlineEdits — append", () => { it("inserts multiple lines", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "append", pos: makeTag(1, "aaa"), lines: ["x", "y", "z"] }]; + const edits: HashlineEdit[] = [{ op: "append_at", pos: makeTag(1, "aaa"), lines: ["x", "y", "z"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\nx\ny\nz\nbbb"); @@ -367,7 +367,7 @@ describe("applyHashlineEdits — append", () => { it("inserts after last line", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "append", pos: makeTag(2, "bbb"), lines: ["NEW"] }]; + const edits: HashlineEdit[] = [{ op: "append_at", pos: makeTag(2, "bbb"), lines: ["NEW"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\nbbb\nNEW"); @@ -375,7 +375,7 @@ describe("applyHashlineEdits — append", () => { it("insert with empty dst inserts an empty line", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "append", pos: makeTag(1, "aaa"), lines: [] }]; + const edits: HashlineEdit[] = [{ op: "append_at", pos: makeTag(1, "aaa"), lines: [] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\n\nbbb"); @@ -384,7 +384,7 @@ describe("applyHashlineEdits — append", () => { it("inserts at EOF without anchors", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "append_eof", lines: ["NEW"] }]; + const edits: HashlineEdit[] = [{ op: "append_file", lines: ["NEW"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\nbbb\nNEW"); @@ -393,7 +393,7 @@ describe("applyHashlineEdits — append", () => { it("inserts at EOF into empty file without anchors", () => { const content = ""; - const edits: HashlineEdit[] = [{ op: "append_eof", lines: ["NEW"] }]; + const edits: HashlineEdit[] = [{ op: "append_file", lines: ["NEW"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("NEW"); @@ -402,7 +402,7 @@ describe("applyHashlineEdits — append", () => { it("insert at EOF with empty dst inserts a trailing empty line", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "append_eof", lines: [] }]; + const edits: HashlineEdit[] = [{ op: "append_file", lines: [] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\nbbb\n"); @@ -417,7 +417,7 @@ describe("applyHashlineEdits — append", () => { describe("applyHashlineEdits — prepend", () => { it("inserts before a line", () => { const content = "aaa\nbbb\nccc"; - const edits: HashlineEdit[] = [{ op: "prepend", pos: makeTag(2, "bbb"), lines: ["NEW"] }]; + const edits: HashlineEdit[] = [{ op: "prepend_at", pos: makeTag(2, "bbb"), lines: ["NEW"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\nNEW\nbbb\nccc"); expect(result.firstChangedLine).toBe(2); @@ -425,21 +425,21 @@ describe("applyHashlineEdits — prepend", () => { it("inserts multiple lines before", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "prepend", pos: makeTag(2, "bbb"), lines: ["x", "y", "z"] }]; + const edits: HashlineEdit[] = [{ op: "prepend_at", pos: makeTag(2, "bbb"), lines: ["x", "y", "z"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\nx\ny\nz\nbbb"); }); it("inserts before first line", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "prepend", pos: makeTag(1, "aaa"), lines: ["NEW"] }]; + const edits: HashlineEdit[] = [{ op: "prepend_at", pos: makeTag(1, "aaa"), lines: ["NEW"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("NEW\naaa\nbbb"); }); it("prepends at BOF without anchor", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "prepend_bof", lines: ["NEW"] }]; + const edits: HashlineEdit[] = [{ op: "prepend_file", lines: ["NEW"] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("NEW\naaa\nbbb"); expect(result.firstChangedLine).toBe(1); @@ -447,7 +447,7 @@ describe("applyHashlineEdits — prepend", () => { it("insert with before and empty text inserts an empty line", () => { const content = "aaa\nbbb"; - const edits: HashlineEdit[] = [{ op: "prepend", pos: makeTag(1, "aaa"), lines: [] }]; + const edits: HashlineEdit[] = [{ op: "prepend_at", pos: makeTag(1, "aaa"), lines: [] }]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("\naaa\nbbb"); expect(result.firstChangedLine).toBe(1); @@ -456,8 +456,8 @@ describe("applyHashlineEdits — prepend", () => { it("insert before and insert after at same line produce correct order", () => { const content = "aaa\nbbb\nccc"; const edits: HashlineEdit[] = [ - { op: "prepend", pos: makeTag(2, "bbb"), lines: ["BEFORE"] }, - { op: "append", pos: makeTag(2, "bbb"), lines: ["AFTER"] }, + { op: "prepend_at", pos: makeTag(2, "bbb"), lines: ["BEFORE"] }, + { op: "append_at", pos: makeTag(2, "bbb"), lines: ["AFTER"] }, ]; const result = applyHashlineEdits(content, edits); expect(result.lines).toBe("aaa\nBEFORE\nbbb\nAFTER\nccc"); @@ -466,7 +466,7 @@ describe("applyHashlineEdits — prepend", () => { it("insert before with set at same line", () => { const content = "aaa\nbbb\nccc"; const edits: HashlineEdit[] = [ - { op: "prepend", pos: makeTag(2, "bbb"), lines: ["BEFORE"] }, + { op: "prepend_at", pos: makeTag(2, "bbb"), lines: ["BEFORE"] }, { op: "replace_line", pos: makeTag(2, "bbb"), lines: ["BBB"] }, ]; const result = applyHashlineEdits(content, edits); @@ -666,7 +666,7 @@ describe("applyHashlineEdits — multiple edits", () => { const content = "aaa\nbbb\nccc"; const edits: HashlineEdit[] = [ { op: "replace_line", pos: makeTag(3, "ccc"), lines: ["CCC"] }, - { op: "append", pos: makeTag(1, "aaa"), lines: ["INSERTED"] }, + { op: "append_at", pos: makeTag(1, "aaa"), lines: ["INSERTED"] }, ]; const result = applyHashlineEdits(content, edits); @@ -805,10 +805,10 @@ describe("applyHashlineEdits — errors", () => { it("accepts append/prepend with empty text by inserting empty lines", () => { const content = "aaa\nbbb"; - const appendEdits: HashlineEdit[] = [{ op: "append", pos: makeTag(1, "aaa"), lines: [] }]; + const appendEdits: HashlineEdit[] = [{ op: "append_at", pos: makeTag(1, "aaa"), lines: [] }]; expect(applyHashlineEdits(content, appendEdits).lines).toBe("aaa\n\nbbb"); - const prependEdits: HashlineEdit[] = [{ op: "prepend", pos: makeTag(1, "aaa"), lines: [] }]; + const prependEdits: HashlineEdit[] = [{ op: "prepend_at", pos: makeTag(1, "aaa"), lines: [] }]; expect(applyHashlineEdits(content, prependEdits).lines).toBe("\naaa\nbbb"); }); });