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:
@@ -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
|
||||
|
||||
@@ -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: "" });
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user