diff --git a/src/tools/hashline-edit/edit-deduplication.ts b/src/tools/hashline-edit/edit-deduplication.ts index d6ccaa1b2..e689bb53a 100644 --- a/src/tools/hashline-edit/edit-deduplication.ts +++ b/src/tools/hashline-edit/edit-deduplication.ts @@ -6,23 +6,13 @@ function normalizeEditPayload(payload: string | string[]): string { } function buildDedupeKey(edit: HashlineEdit): string { - switch (edit.type) { - case "set_line": - return `set_line|${edit.line}|${normalizeEditPayload(edit.text)}` - case "replace_lines": - return `replace_lines|${edit.start_line}|${edit.end_line}|${normalizeEditPayload(edit.text)}` - case "insert_after": - return `insert_after|${edit.line}|${normalizeEditPayload(edit.text)}` - case "insert_before": - return `insert_before|${edit.line}|${normalizeEditPayload(edit.text)}` - case "insert_between": - return `insert_between|${edit.after_line}|${edit.before_line}|${normalizeEditPayload(edit.text)}` + switch (edit.op) { case "replace": - return `replace|${edit.old_text}|${normalizeEditPayload(edit.new_text)}` + return `replace|${edit.pos}|${edit.end ?? ""}|${normalizeEditPayload(edit.lines)}` case "append": - return `append|${normalizeEditPayload(edit.text)}` + return `append|${edit.pos ?? ""}|${normalizeEditPayload(edit.lines)}` case "prepend": - return `prepend|${normalizeEditPayload(edit.text)}` + return `prepend|${edit.pos ?? ""}|${normalizeEditPayload(edit.lines)}` default: return JSON.stringify(edit) } diff --git a/src/tools/hashline-edit/edit-operation-primitives.ts b/src/tools/hashline-edit/edit-operation-primitives.ts index efd88f79c..fc07c6113 100644 --- a/src/tools/hashline-edit/edit-operation-primitives.ts +++ b/src/tools/hashline-edit/edit-operation-primitives.ts @@ -150,11 +150,3 @@ export function applyPrepend(lines: string[], text: string | string[]): string[] } return [...normalized, ...lines] } - -export function applyReplace(content: string, oldText: string, newText: string | string[]): string { - if (!content.includes(oldText)) { - throw new Error(`Text not found: "${oldText}"`) - } - const replacement = Array.isArray(newText) ? newText.join("\n") : newText - return content.replaceAll(oldText, replacement) -} diff --git a/src/tools/hashline-edit/edit-operations.test.ts b/src/tools/hashline-edit/edit-operations.test.ts index d169cb72f..24c0cdce2 100644 --- a/src/tools/hashline-edit/edit-operations.test.ts +++ b/src/tools/hashline-edit/edit-operations.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test" -import { applyHashlineEdits, applyInsertAfter, applyReplace, applyReplaceLines, applySetLine } from "./edit-operations" -import { applyAppend, applyPrepend } from "./edit-operation-primitives" +import { applyHashlineEdits, applyInsertAfter, applyReplaceLines, applySetLine } from "./edit-operations" +import { applyAppend, applyInsertBetween, applyPrepend } from "./edit-operation-primitives" import { computeLineHash } from "./hash-computation" import type { HashlineEdit } from "./types" @@ -49,7 +49,7 @@ describe("hashline edit operations", () => { //#when const result = applyHashlineEdits( lines.join("\n"), - [{ type: "insert_before", line: anchorFor(lines, 2), text: "before 2" }] + [{ op: "prepend", pos: anchorFor(lines, 2), lines: "before 2" }] ) //#then @@ -61,15 +61,7 @@ describe("hashline edit operations", () => { const lines = ["line 1", "line 2", "line 3"] //#when - const result = applyHashlineEdits( - lines.join("\n"), - [{ - type: "insert_between", - after_line: anchorFor(lines, 1), - before_line: anchorFor(lines, 2), - text: ["between"], - }] - ) + const result = applyInsertBetween(lines, anchorFor(lines, 1), anchorFor(lines, 2), ["between"]).join("\n") //#then expect(result).toEqual("line 1\nbetween\nline 2\nline 3") @@ -89,7 +81,7 @@ describe("hashline edit operations", () => { //#when / #then expect(() => - applyHashlineEdits(lines.join("\n"), [{ type: "insert_before", line: anchorFor(lines, 1), text: [] }]) + applyHashlineEdits(lines.join("\n"), [{ op: "prepend", pos: anchorFor(lines, 1), lines: [] }]) ).toThrow(/non-empty/i) }) @@ -98,28 +90,7 @@ describe("hashline edit operations", () => { const lines = ["line 1", "line 2"] //#when / #then - expect(() => - applyHashlineEdits( - lines.join("\n"), - [{ - type: "insert_between", - after_line: anchorFor(lines, 1), - before_line: anchorFor(lines, 2), - text: [], - }] - ) - ).toThrow(/non-empty/i) - }) - - it("applies replace operation", () => { - //#given - const content = "hello world foo" - - //#when - const result = applyReplace(content, "world", "universe") - - //#then - expect(result).toEqual("hello universe foo") + expect(() => applyInsertBetween(lines, anchorFor(lines, 1), anchorFor(lines, 2), [])).toThrow(/non-empty/i) }) it("applies mixed edits in one pass", () => { @@ -127,8 +98,8 @@ describe("hashline edit operations", () => { const content = "line 1\nline 2\nline 3" const lines = content.split("\n") const edits: HashlineEdit[] = [ - { type: "insert_after", line: anchorFor(lines, 1), text: "inserted" }, - { type: "set_line", line: anchorFor(lines, 3), text: "modified" }, + { op: "append", pos: anchorFor(lines, 1), lines: "inserted" }, + { op: "replace", pos: anchorFor(lines, 3), lines: "modified" }, ] //#when @@ -143,8 +114,8 @@ describe("hashline edit operations", () => { const content = "line 1\nline 2" const lines = content.split("\n") const edits: HashlineEdit[] = [ - { type: "insert_after", line: anchorFor(lines, 1), text: "inserted" }, - { type: "insert_after", line: anchorFor(lines, 1), text: "inserted" }, + { op: "append", pos: anchorFor(lines, 1), lines: "inserted" }, + { op: "append", pos: anchorFor(lines, 1), lines: "inserted" }, ] //#when @@ -227,16 +198,9 @@ describe("hashline edit operations", () => { const lines = ["line 1", "line 2", "line 3"] //#when / #then - expect(() => - applyHashlineEdits(lines.join("\n"), [ - { - type: "insert_between", - after_line: anchorFor(lines, 1), - before_line: anchorFor(lines, 2), - text: ["line 1", "line 2"], - }, - ]) - ).toThrow(/non-empty/i) + expect(() => applyInsertBetween(lines, anchorFor(lines, 1), anchorFor(lines, 2), ["line 1", "line 2"])).toThrow( + /non-empty/i + ) }) it("restores indentation for first replace_lines entry", () => { @@ -322,8 +286,8 @@ describe("hashline edit operations", () => { //#when const result = applyHashlineEdits(content, [ - { type: "append", text: ["line 3"] }, - { type: "prepend", text: ["line 0"] }, + { op: "append", lines: ["line 3"] }, + { op: "prepend", lines: ["line 0"] }, ]) //#then diff --git a/src/tools/hashline-edit/edit-operations.ts b/src/tools/hashline-edit/edit-operations.ts index 7e2279a25..0aa2e5390 100644 --- a/src/tools/hashline-edit/edit-operations.ts +++ b/src/tools/hashline-edit/edit-operations.ts @@ -5,9 +5,7 @@ import { applyAppend, applyInsertAfter, applyInsertBefore, - applyInsertBetween, applyPrepend, - applyReplace, applyReplaceLines, applySetLine, } from "./edit-operation-primitives" @@ -33,42 +31,17 @@ export function applyHashlineEditsWithReport(content: string, edits: HashlineEdi let noopEdits = 0 - let result = content - let lines = result.length === 0 ? [] : result.split("\n") + let lines = content.length === 0 ? [] : content.split("\n") const refs = collectLineRefs(sortedEdits) validateLineRefs(lines, refs) for (const edit of sortedEdits) { - switch (edit.type) { - case "set_line": { - lines = applySetLine(lines, edit.line, edit.text, { skipValidation: true }) - break - } - case "replace_lines": { - lines = applyReplaceLines(lines, edit.start_line, edit.end_line, edit.text, { skipValidation: true }) - break - } - case "insert_after": { - const next = applyInsertAfter(lines, edit.line, edit.text, { skipValidation: true }) - if (next.join("\n") === lines.join("\n")) { - noopEdits += 1 - break - } - lines = next - break - } - case "insert_before": { - const next = applyInsertBefore(lines, edit.line, edit.text, { skipValidation: true }) - if (next.join("\n") === lines.join("\n")) { - noopEdits += 1 - break - } - lines = next - break - } - case "insert_between": { - const next = applyInsertBetween(lines, edit.after_line, edit.before_line, edit.text, { skipValidation: true }) + switch (edit.op) { + case "replace": { + const next = edit.end + ? applyReplaceLines(lines, edit.pos, edit.end, edit.lines, { skipValidation: true }) + : applySetLine(lines, edit.pos, edit.lines, { skipValidation: true }) if (next.join("\n") === lines.join("\n")) { noopEdits += 1 break @@ -77,7 +50,9 @@ export function applyHashlineEditsWithReport(content: string, edits: HashlineEdi break } case "append": { - const next = applyAppend(lines, edit.text) + const next = edit.pos + ? applyInsertAfter(lines, edit.pos, edit.lines, { skipValidation: true }) + : applyAppend(lines, edit.lines) if (next.join("\n") === lines.join("\n")) { noopEdits += 1 break @@ -86,7 +61,9 @@ export function applyHashlineEditsWithReport(content: string, edits: HashlineEdi break } case "prepend": { - const next = applyPrepend(lines, edit.text) + const next = edit.pos + ? applyInsertBefore(lines, edit.pos, edit.lines, { skipValidation: true }) + : applyPrepend(lines, edit.lines) if (next.join("\n") === lines.join("\n")) { noopEdits += 1 break @@ -94,17 +71,6 @@ export function applyHashlineEditsWithReport(content: string, edits: HashlineEdi lines = next break } - case "replace": { - result = lines.join("\n") - const replaced = applyReplace(result, edit.old_text, edit.new_text) - if (replaced === result) { - noopEdits += 1 - break - } - result = replaced - lines = result.split("\n") - break - } } } @@ -124,6 +90,4 @@ export { applyReplaceLines, applyInsertAfter, applyInsertBefore, - applyInsertBetween, - applyReplace, } from "./edit-operation-primitives" diff --git a/src/tools/hashline-edit/edit-ordering.ts b/src/tools/hashline-edit/edit-ordering.ts index d8196babb..f56587798 100644 --- a/src/tools/hashline-edit/edit-ordering.ts +++ b/src/tools/hashline-edit/edit-ordering.ts @@ -2,23 +2,13 @@ import { parseLineRef } from "./validation" import type { HashlineEdit } from "./types" export function getEditLineNumber(edit: HashlineEdit): number { - switch (edit.type) { - case "set_line": - return parseLineRef(edit.line).line - case "replace_lines": - return parseLineRef(edit.end_line).line - case "insert_after": - return parseLineRef(edit.line).line - case "insert_before": - return parseLineRef(edit.line).line - case "insert_between": - return parseLineRef(edit.before_line).line - case "append": - return Number.NEGATIVE_INFINITY - case "prepend": - return Number.NEGATIVE_INFINITY + switch (edit.op) { case "replace": - return Number.NEGATIVE_INFINITY + return parseLineRef(edit.end ?? edit.pos).line + case "append": + return edit.pos ? parseLineRef(edit.pos).line : Number.NEGATIVE_INFINITY + case "prepend": + return edit.pos ? parseLineRef(edit.pos).line : Number.NEGATIVE_INFINITY default: return Number.POSITIVE_INFINITY } @@ -26,21 +16,12 @@ export function getEditLineNumber(edit: HashlineEdit): number { export function collectLineRefs(edits: HashlineEdit[]): string[] { return edits.flatMap((edit) => { - switch (edit.type) { - case "set_line": - return [edit.line] - case "replace_lines": - return [edit.start_line, edit.end_line] - case "insert_after": - return [edit.line] - case "insert_before": - return [edit.line] - case "insert_between": - return [edit.after_line, edit.before_line] + switch (edit.op) { + case "replace": + return edit.end ? [edit.pos, edit.end] : [edit.pos] case "append": case "prepend": - case "replace": - return [] + return edit.pos ? [edit.pos] : [] default: return [] } diff --git a/src/tools/hashline-edit/hashline-edit-executor.ts b/src/tools/hashline-edit/hashline-edit-executor.ts index 777ae1622..bbbba4ac1 100644 --- a/src/tools/hashline-edit/hashline-edit-executor.ts +++ b/src/tools/hashline-edit/hashline-edit-executor.ts @@ -32,7 +32,7 @@ function resolveToolCallID(ctx: ToolContextWithCallID): string | undefined { function canCreateFromMissingFile(edits: HashlineEdit[]): boolean { if (edits.length === 0) return false - return edits.every((edit) => edit.type === "append" || edit.type === "prepend") + return edits.every((edit) => edit.op === "append" || edit.op === "prepend") } function buildSuccessMeta( diff --git a/src/tools/hashline-edit/index.ts b/src/tools/hashline-edit/index.ts index d76f7e1f7..97a0ba7b2 100644 --- a/src/tools/hashline-edit/index.ts +++ b/src/tools/hashline-edit/index.ts @@ -8,14 +8,9 @@ export { export { parseLineRef, validateLineRef } from "./validation" export type { LineRef } from "./validation" export type { - SetLine, - ReplaceLines, - InsertAfter, - InsertBefore, - InsertBetween, - Replace, - Append, - Prepend, + ReplaceEdit, + AppendEdit, + PrependEdit, HashlineEdit, } from "./types" export { NIBBLE_STR, HASHLINE_DICT, HASHLINE_REF_PATTERN, HASHLINE_OUTPUT_PATTERN } from "./constants" @@ -23,8 +18,6 @@ export { applyHashlineEdits, applyInsertAfter, applyInsertBefore, - applyInsertBetween, - applyReplace, applyReplaceLines, applySetLine, } from "./edit-operations" diff --git a/src/tools/hashline-edit/normalize-edits.test.ts b/src/tools/hashline-edit/normalize-edits.test.ts index 87cd05a62..45cf6f253 100644 --- a/src/tools/hashline-edit/normalize-edits.test.ts +++ b/src/tools/hashline-edit/normalize-edits.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test" import { normalizeHashlineEdits, type RawHashlineEdit } from "./normalize-edits" describe("normalizeHashlineEdits", () => { - it("maps replace with pos to set_line", () => { + it("maps replace with pos to replace", () => { //#given const input: RawHashlineEdit[] = [{ op: "replace", pos: "2#VK", lines: "updated" }] @@ -10,10 +10,10 @@ describe("normalizeHashlineEdits", () => { const result = normalizeHashlineEdits(input) //#then - expect(result).toEqual([{ type: "set_line", line: "2#VK", text: "updated" }]) + expect(result).toEqual([{ op: "replace", pos: "2#VK", lines: "updated" }]) }) - it("maps replace with pos and end to replace_lines", () => { + it("maps replace with pos and end to replace", () => { //#given const input: RawHashlineEdit[] = [{ op: "replace", pos: "2#VK", end: "4#MB", lines: ["a", "b"] }] @@ -21,10 +21,10 @@ describe("normalizeHashlineEdits", () => { const result = normalizeHashlineEdits(input) //#then - expect(result).toEqual([{ type: "replace_lines", start_line: "2#VK", end_line: "4#MB", text: ["a", "b"] }]) + expect(result).toEqual([{ op: "replace", pos: "2#VK", end: "4#MB", lines: ["a", "b"] }]) }) - it("maps anchored append and prepend to insert operations", () => { + it("maps anchored append and prepend preserving op", () => { //#given const input: RawHashlineEdit[] = [ { op: "append", pos: "2#VK", lines: ["after"] }, @@ -35,10 +35,7 @@ describe("normalizeHashlineEdits", () => { const result = normalizeHashlineEdits(input) //#then - expect(result).toEqual([ - { type: "insert_after", line: "2#VK", text: ["after"] }, - { type: "insert_before", line: "4#MB", text: ["before"] }, - ]) + expect(result).toEqual([{ op: "append", pos: "2#VK", lines: ["after"] }, { op: "prepend", pos: "4#MB", lines: ["before"] }]) }) it("prefers pos over end for prepend anchors", () => { @@ -49,7 +46,7 @@ describe("normalizeHashlineEdits", () => { const result = normalizeHashlineEdits(input) //#then - expect(result).toEqual([{ type: "insert_before", line: "3#AA", text: ["before"] }]) + expect(result).toEqual([{ op: "prepend", pos: "3#AA", lines: ["before"] }]) }) it("rejects legacy payload without op", () => { diff --git a/src/tools/hashline-edit/normalize-edits.ts b/src/tools/hashline-edit/normalize-edits.ts index 913b86ed0..03b28f7c4 100644 --- a/src/tools/hashline-edit/normalize-edits.ts +++ b/src/tools/hashline-edit/normalize-edits.ts @@ -1,4 +1,4 @@ -import type { HashlineEdit } from "./types" +import type { AppendEdit, HashlineEdit, PrependEdit, ReplaceEdit } from "./types" type HashlineToolOp = "replace" | "append" | "prepend" @@ -36,62 +36,43 @@ function normalizeReplaceEdit(edit: RawHashlineEdit, index: number): HashlineEdi const pos = normalizeAnchor(edit.pos) const end = normalizeAnchor(edit.end) const anchor = requireLine(pos ?? end, index, "replace") - const text = requireLines(edit, index) + const lines = requireLines(edit, index) - if (pos && end) { - return { - type: "replace_lines", - start_line: pos, - end_line: end, - text, - } - } - - return { - type: "set_line", - line: anchor, - text, + const normalized: ReplaceEdit = { + op: "replace", + pos: anchor, + lines, } + if (end) normalized.end = end + return normalized } function normalizeAppendEdit(edit: RawHashlineEdit, index: number): HashlineEdit { const pos = normalizeAnchor(edit.pos) const end = normalizeAnchor(edit.end) const anchor = pos ?? end - const text = requireLines(edit, index) + const lines = requireLines(edit, index) - if (!anchor) { - return { - type: "append", - text, - } - } - - return { - type: "insert_after", - line: anchor, - text, + const normalized: AppendEdit = { + op: "append", + lines, } + if (anchor) normalized.pos = anchor + return normalized } function normalizePrependEdit(edit: RawHashlineEdit, index: number): HashlineEdit { const pos = normalizeAnchor(edit.pos) const end = normalizeAnchor(edit.end) const anchor = pos ?? end - const text = requireLines(edit, index) + const lines = requireLines(edit, index) - if (!anchor) { - return { - type: "prepend", - text, - } - } - - return { - type: "insert_before", - line: anchor, - text, + const normalized: PrependEdit = { + op: "prepend", + lines, } + if (anchor) normalized.pos = anchor + return normalized } export function normalizeHashlineEdits(rawEdits: RawHashlineEdit[]): HashlineEdit[] { diff --git a/src/tools/hashline-edit/types.ts b/src/tools/hashline-edit/types.ts index f69f27570..e0fc8485d 100644 --- a/src/tools/hashline-edit/types.ts +++ b/src/tools/hashline-edit/types.ts @@ -1,57 +1,20 @@ -export interface SetLine { - type: "set_line" - line: string - text: string | string[] +export interface ReplaceEdit { + op: "replace" + pos: string + end?: string + lines: string | string[] } -export interface ReplaceLines { - type: "replace_lines" - start_line: string - end_line: string - text: string | string[] +export interface AppendEdit { + op: "append" + pos?: string + lines: string | string[] } -export interface InsertAfter { - type: "insert_after" - line: string - text: string | string[] +export interface PrependEdit { + op: "prepend" + pos?: string + lines: string | string[] } -export interface InsertBefore { - type: "insert_before" - line: string - text: string | string[] -} - -export interface InsertBetween { - type: "insert_between" - after_line: string - before_line: string - text: string | string[] -} - -export interface Replace { - type: "replace" - old_text: string - new_text: string | string[] -} - -export interface Append { - type: "append" - text: string | string[] -} - -export interface Prepend { - type: "prepend" - text: string | string[] -} - -export type HashlineEdit = - | SetLine - | ReplaceLines - | InsertAfter - | InsertBefore - | InsertBetween - | Replace - | Append - | Prepend +export type HashlineEdit = ReplaceEdit | AppendEdit | PrependEdit