diff --git a/packages/coding-agent/src/hashline/apply.ts b/packages/coding-agent/src/hashline/apply.ts index a81d66e69..315e32785 100644 --- a/packages/coding-agent/src/hashline/apply.ts +++ b/packages/coding-agent/src/hashline/apply.ts @@ -1,6 +1,6 @@ import { HashlineMismatchError } from "./anchors"; import { RANGE_INTERIOR_HASH } from "./constants"; -import { computeLineHash, HL_EDIT_SEP } from "./hash"; +import { computeLineHash } from "./hash"; import { cloneCursor } from "./parser"; import type { Anchor, HashlineApplyOptions, HashlineCursor, HashlineEdit, HashMismatch } from "./types"; @@ -37,7 +37,6 @@ interface HashlineReplacementGroup { function getHashlineEditAnchors(edit: HashlineEdit): Anchor[] { if (edit.kind === "delete") return [edit.anchor]; - if (edit.kind === "modify") return [edit.anchor]; if (edit.cursor.kind === "before_anchor") return [edit.cursor.anchor]; if (edit.cursor.kind === "after_anchor") return [edit.cursor.anchor]; return []; @@ -96,7 +95,7 @@ function insertAtEnd(fileLines: string[], lineOrigins: HashlineLineOrigin[], lin /** Bucket edits by the line they target so we can apply each line's group in one splice. */ function getAnchorTargetLine(edit: HashlineEdit): number | undefined { - if (edit.kind === "delete" || edit.kind === "modify") return edit.anchor.line; + if (edit.kind === "delete") return edit.anchor.line; if (edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor") return edit.cursor.anchor.line; return undefined; } @@ -607,11 +606,9 @@ function bucketAnchorEditsByLine(edits: IndexedEdit[]): Map (deleteLine ? "replacement" : "insert")); if (!deleteLine) { - origins[origins.length - 1] = modified ? "replacement" : (lineOrigins[idx] ?? "original"); + origins[origins.length - 1] = lineOrigins[idx] ?? "original"; } fileLines.splice(idx, 1, ...replacement); diff --git a/packages/coding-agent/src/hashline/execute.ts b/packages/coding-agent/src/hashline/execute.ts index d14cf26ea..1cf666712 100644 --- a/packages/coding-agent/src/hashline/execute.ts +++ b/packages/coding-agent/src/hashline/execute.ts @@ -42,7 +42,6 @@ async function readHashlineFile(absolutePath: string, pathText: string): Promise function hasAnchorScopedEdit(edits: HashlineEdit[]): boolean { return edits.some(edit => { if (edit.kind === "delete") return true; - if (edit.kind === "modify") return true; return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor"; }); } diff --git a/packages/coding-agent/src/hashline/grammar.lark b/packages/coding-agent/src/hashline/grammar.lark index 5e248bb8b..70fcc5e2e 100644 --- a/packages/coding-agent/src/hashline/grammar.lark +++ b/packages/coding-agent/src/hashline/grammar.lark @@ -7,16 +7,12 @@ section: file_header line_op* file_header: "@" path LF -line_op: inline_before_op payload* - | inline_after_op payload* - | insert_before_op payload+ +line_op: insert_before_op payload+ | insert_after_op payload+ | replace_op payload* | delete_op | blank -inline_before_op: "<" LID $HSEP$ line_text? LF -inline_after_op: "+" LID $HSEP$ line_text? LF insert_before_op: "<" insert_target LF insert_after_op: "+" insert_target LF replace_op: "=" range LF diff --git a/packages/coding-agent/src/hashline/parser.ts b/packages/coding-agent/src/hashline/parser.ts index 5489780c3..00b3df3a4 100644 --- a/packages/coding-agent/src/hashline/parser.ts +++ b/packages/coding-agent/src/hashline/parser.ts @@ -1,9 +1,8 @@ import { RANGE_INTERIOR_HASH } from "./constants"; -import { describeAnchorExamples, HL_EDIT_SEP, HL_EDIT_SEP_RE_RAW, HL_HASH_CAPTURE_RE_RAW } from "./hash"; +import { describeAnchorExamples, HL_EDIT_SEP, HL_HASH_CAPTURE_RE_RAW } from "./hash"; import type { Anchor, HashlineCursor, HashlineEdit } from "./types"; import { stripTrailingCarriageReturn } from "./utils"; -const HL_EDIT_SEPARATOR_RE = HL_EDIT_SEP_RE_RAW; const LID_CAPTURE_RE = new RegExp(`^${HL_HASH_CAPTURE_RE_RAW}$`); function parseLid(raw: string, lineNum: number): Anchor { @@ -70,8 +69,6 @@ const INSERT_BEFORE_OP_RE = /^<\s*(\S+)$/; const INSERT_AFTER_OP_RE = /^\+\s*(\S+)$/; const DELETE_OP_RE = /^-\s*(\S+)$/; const REPLACE_OP_RE = /^=\s*(\S+)$/; -const INLINE_BEFORE_OP_RE = new RegExp(`^<\\s*${HL_HASH_CAPTURE_RE_RAW}${HL_EDIT_SEPARATOR_RE}(.*)$`); -const INLINE_AFTER_OP_RE = new RegExp(`^\\+\\s*${HL_HASH_CAPTURE_RE_RAW}${HL_EDIT_SEPARATOR_RE}(.*)$`); export function cloneCursor(cursor: HashlineCursor): HashlineCursor { if (cursor.kind === "before_anchor") return { kind: "before_anchor", anchor: { ...cursor.anchor } }; @@ -125,42 +122,6 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[] throw new Error(`line ${lineNum}: payload line has no preceding +, <, or = operation.`); } - const inlineBeforeMatch = INLINE_BEFORE_OP_RE.exec(line); - if (inlineBeforeMatch) { - const anchor = parseLid(`${inlineBeforeMatch[1]}${inlineBeforeMatch[2]}`, lineNum); - edits.push({ - kind: "modify", - anchor, - prefix: inlineBeforeMatch[3], - suffix: "", - lineNum, - index: editIndex++, - }); - const cursor: HashlineCursor = { kind: "before_anchor", anchor }; - const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false); - for (const text of payload) pushInsert(cursor, text, lineNum); - i = nextIndex; - continue; - } - - const inlineAfterMatch = INLINE_AFTER_OP_RE.exec(line); - if (inlineAfterMatch) { - const anchor = parseLid(`${inlineAfterMatch[1]}${inlineAfterMatch[2]}`, lineNum); - edits.push({ - kind: "modify", - anchor, - prefix: "", - suffix: inlineAfterMatch[3], - lineNum, - index: editIndex++, - }); - const cursor: HashlineCursor = { kind: "after_anchor", anchor }; - const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false); - for (const text of payload) pushInsert(cursor, text, lineNum); - i = nextIndex; - continue; - } - const insertBeforeMatch = INSERT_BEFORE_OP_RE.exec(line); if (insertBeforeMatch) { const cursor = parseInsertTarget(insertBeforeMatch[1], lineNum, "before"); diff --git a/packages/coding-agent/src/hashline/types.ts b/packages/coding-agent/src/hashline/types.ts index 3c6e12c01..2bb963572 100644 --- a/packages/coding-agent/src/hashline/types.ts +++ b/packages/coding-agent/src/hashline/types.ts @@ -24,8 +24,7 @@ export type HashlineCursor = export type HashlineEdit = | { kind: "insert"; cursor: HashlineCursor; text: string; lineNum: number; index: number } - | { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string } - | { kind: "modify"; anchor: Anchor; prefix: string; suffix: string; lineNum: number; index: number }; + | { kind: "delete"; anchor: Anchor; lineNum: number; index: number; oldAssertion?: string }; export const hashlineEditParamsSchema = Type.Object({ input: Type.String() }); export type HashlineParams = Static; diff --git a/packages/coding-agent/src/prompts/tools/hashline.md b/packages/coding-agent/src/prompts/tools/hashline.md index 6a51c9ccd..db3fcabc4 100644 --- a/packages/coding-agent/src/prompts/tools/hashline.md +++ b/packages/coding-agent/src/prompts/tools/hashline.md @@ -96,10 +96,6 @@ When your edit involves brace boundaries (`{` / `}`), prefer these shapes: + {{hrefr 4}} {{hsep}} if (clean.length === 0) return DEF; -# Append WITHIN a line -@a.ts -+ {{hrefr 4}}{{hsep}} // first run - # Append to end of file @a.ts + EOF diff --git a/packages/coding-agent/test/core/hashline.test.ts b/packages/coding-agent/test/core/hashline.test.ts index 4825e0f66..da025e2eb 100644 --- a/packages/coding-agent/test/core/hashline.test.ts +++ b/packages/coding-agent/test/core/hashline.test.ts @@ -328,66 +328,6 @@ describe("hashline parser — block op syntax", () => { }); }); -describe("hashline parser — inline modify syntax", () => { - const content = "alpha\nbeta\ngamma"; - - it("prepends text to the anchored line via `< ANCHORTEXT`", () => { - const diff = `< ${tag(2, "beta")}${pl("// ")}`; - expect(applyDiff(content, diff)).toBe("alpha\n// beta\ngamma"); - }); - - it("appends text to the anchored line via `+ ANCHORTEXT`", () => { - const diff = `+ ${tag(2, "beta")}${pl(" // tag")}`; - expect(applyDiff(content, diff)).toBe("alpha\nbeta // tag\ngamma"); - }); - - it("combines a prepend and an append on the same line", () => { - const diff = [`< ${tag(2, "beta")}${pl("[")}`, `+ ${tag(2, "beta")}${pl("]")}`].join("\n"); - expect(applyDiff(content, diff)).toBe("alpha\n[beta]\ngamma"); - }); - - it("stacks multiple prepends with later edits wrapping earlier ones", () => { - const diff = [`< ${tag(2, "beta")}${pl("A")}`, `< ${tag(2, "beta")}${pl("B")}`].join("\n"); - expect(applyDiff(content, diff)).toBe("alpha\nBAbeta\ngamma"); - }); - - it("stacks multiple appends with later edits wrapping earlier ones", () => { - const diff = [`+ ${tag(2, "beta")}${pl("A")}`, `+ ${tag(2, "beta")}${pl("B")}`].join("\n"); - expect(applyDiff(content, diff)).toBe("alpha\nbetaAB\ngamma"); - }); - - it("appends inline AND inserts payload lines after the modified line", () => { - const diff = [`+ ${tag(2, "beta")}${pl(" // tag")}`, pl("inserted-after-1"), pl("inserted-after-2")].join("\n"); - expect(applyDiff(content, diff)).toBe("alpha\nbeta // tag\ninserted-after-1\ninserted-after-2\ngamma"); - }); - - it("prepends inline AND inserts payload lines before the modified line", () => { - const diff = [`< ${tag(2, "beta")}${pl("// ")}`, pl("inserted-before-1"), pl("inserted-before-2")].join("\n"); - expect(applyDiff(content, diff)).toBe("alpha\ninserted-before-1\ninserted-before-2\n// beta\ngamma"); - }); - - it("allows a block insert-before to coexist with an inline modify on the same line", () => { - const diff = [`< ${tag(2, "beta")}`, pl("// note"), `+ ${tag(2, "beta")}${pl("!")}`].join("\n"); - expect(applyDiff(content, diff)).toBe("alpha\n// note\nbeta!\ngamma"); - }); - - it("rejects combining inline modify with a delete on the same line", () => { - const diff = [`- ${sameLineRange(tag(2, "beta"))}`, `+ ${tag(2, "beta")}${pl("!")}`].join("\n"); - expect(() => applyDiff(content, diff)).toThrow(/cannot combine inline modify/); - }); - - it("validates the anchor hash for inline modify just like other ops", () => { - const diff = `+ ${mistag(2, "beta")}${pl("!")}`; - expect(() => applyDiff(content, diff)).toThrow(HashlineMismatchError); - }); - - it("treats an empty inline payload as a no-op when nothing else follows", () => { - const diff = `+ ${tag(2, "beta")}${pl("")}`; - const result = applyHashlineEdits(content, parseHashline(diff)); - expect(result.lines).toBe(content); - }); -}); - describe("hashline — stale anchors", () => { it("throws HashlineMismatchError when a Lid hash no longer matches", () => { const diff = [`= ${sameLineRange(mistag(2, "bbb"))}`, pl("BBB")].join("\n");