refactor(hashline): removed inline modify edit kind
- Dropped the `modify` edit type and its `< ANCHORTEXT` / `+ ANCHORTEXT` syntax. - Removed associated parser rules, regex patterns, apply logic, and tests. - Deleted the "Append WITHIN a line" example from the prompt docs.
This commit is contained in:
@@ -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<number, IndexedEdit[
|
||||
const line =
|
||||
entry.edit.kind === "delete"
|
||||
? entry.edit.anchor.line
|
||||
: entry.edit.kind === "modify"
|
||||
? entry.edit.anchor.line
|
||||
: entry.edit.cursor.kind === "before_anchor"
|
||||
? entry.edit.cursor.anchor.line
|
||||
: 0;
|
||||
: entry.edit.cursor.kind === "before_anchor"
|
||||
? entry.edit.cursor.anchor.line
|
||||
: 0;
|
||||
const bucket = byLine.get(line);
|
||||
if (bucket) bucket.push(entry);
|
||||
else byLine.set(line, [entry]);
|
||||
@@ -683,33 +680,20 @@ export function applyHashlineEdits(
|
||||
const currentLine = fileLines[idx] ?? "";
|
||||
const beforeLines: string[] = [];
|
||||
let deleteLine = false;
|
||||
let prefix = "";
|
||||
let suffix = "";
|
||||
let modified = false;
|
||||
|
||||
for (const { edit } of bucket) {
|
||||
if (edit.kind === "insert") {
|
||||
beforeLines.push(edit.text);
|
||||
} else if (edit.kind === "delete") {
|
||||
deleteLine = true;
|
||||
} else if (edit.kind === "modify") {
|
||||
prefix = edit.prefix + prefix;
|
||||
suffix = suffix + edit.suffix;
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
if (beforeLines.length === 0 && !deleteLine && !modified) continue;
|
||||
if (deleteLine && modified) {
|
||||
throw new Error(
|
||||
`line ${line}: cannot combine inline modify ("< ${line}${HL_EDIT_SEP}…" or "+ ${line}${HL_EDIT_SEP}…") with a delete or replace targeting the same line.`,
|
||||
);
|
||||
}
|
||||
if (beforeLines.length === 0 && !deleteLine) continue;
|
||||
|
||||
const effectiveLine = modified ? prefix + currentLine + suffix : currentLine;
|
||||
const replacement = deleteLine ? beforeLines : [...beforeLines, effectiveLine];
|
||||
const replacement = deleteLine ? beforeLines : [...beforeLines, currentLine];
|
||||
const origins = replacement.map((): HashlineLineOrigin => (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);
|
||||
|
||||
@@ -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";
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<typeof hashlineEditParamsSchema>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 `< ANCHOR<sep>TEXT`", () => {
|
||||
const diff = `< ${tag(2, "beta")}${pl("// ")}`;
|
||||
expect(applyDiff(content, diff)).toBe("alpha\n// beta\ngamma");
|
||||
});
|
||||
|
||||
it("appends text to the anchored line via `+ ANCHOR<sep>TEXT`", () => {
|
||||
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");
|
||||
|
||||
Reference in New Issue
Block a user