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.
This commit is contained in:
can1357
2026-03-22 21:14:31 +01:00
parent 66a478e6e6
commit 012e0c90b9
5 changed files with 106 additions and 164 deletions
+1 -1
View File
@@ -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
+20 -20
View File
@@ -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: "" });
+22 -22
View File
@@ -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<typeof hashlineEditParamsSchema>;
* 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<TInput> {
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<TInput> {
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<TInput> {
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;
}
@@ -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.
<operations>
**`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
</operations>
<examples>
@@ -47,8 +56,7 @@ All examples below reference the same file, `util.ts`:
{{hlinefull 18 "}"}}
```
<example name="single-line replace">
Change the timeout from `5000` to `30_000`:
<example name="replace one line">
```
{
path: "util.ts",
@@ -61,19 +69,7 @@ Change the timeout from `5000` to `30_000`:
```
</example>
<example name="delete lines">
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):
<example name="delete a range">
```
{
path: "util.ts",
@@ -87,10 +83,8 @@ Range — remove the legacy block (lines 10–11):
```
</example>
<example name="rewrite a block body — shape (a)">
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.
<example name="replace a block body">
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
```
</example>
<example name="replace whole block — shape (b)">
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.
<example name="insert before sibling">
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);",
"}"
]
}]
}
```
</example>
<example name="avoid shared boundary lines">
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
</example>
<example name="insert between sibling declarations">
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.
</example>
</examples>
<critical>
- 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.
</critical>
@@ -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");
});
});