feat: replaced legacy editing commands with unified put and cut syntax

- Replaced legacy `SWAP`, `INS`, and `PASTE` commands with unified `PUT` and `CUT` hunks across parser, grammar, tokenizer, and test suites.
- Added support for named registers and span paste operations in clipboard and block execution logic.
- Implemented indentation repair and enhanced gap locator formatting for improved patch resilience.
- Updated documentation, system prompts, and session analysis scripts to reflect the new syntax and header shapes.
This commit is contained in:
can1357
2026-07-31 00:19:52 +02:00
parent 55b23864e2
commit 5ea583e413
46 changed files with 1767 additions and 1243 deletions
@@ -106,7 +106,7 @@ describe("read → edit seen-line guard", () => {
const tag = tagFromOutput(resultText(read));
await expect(
executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 12.=12:\n+EDITED`, session)),
executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 12-12:\n+EDITED`, session)),
).rejects.toThrow(/never displayed \(it showed/);
// The reject left the file untouched.
expect(await Bun.file(file).text()).toBe(CONTENT);
@@ -120,7 +120,7 @@ describe("read → edit seen-line guard", () => {
const read = await new ReadTool(session).execute("r1", { path: `${file}:1-3` });
const tag = tagFromOutput(resultText(read));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 2.=2:\n+EDITED`, session));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 2-2:\n+EDITED`, session));
expect(await Bun.file(file).text()).toContain("EDITED");
});
@@ -143,7 +143,7 @@ describe("read → edit seen-line guard", () => {
expect(seen?.has(6)).toBe(true);
expect(seen?.has(10)).toBe(false);
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 5.=5:\n+RAW EDITED`, session));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 5-5:\n+RAW EDITED`, session));
const edited = await Bun.file(file).text();
expect(edited).toContain("line 4\nRAW EDITED\nline 6");
expect(edited).not.toContain("line 5");
@@ -169,11 +169,11 @@ describe("read → edit seen-line guard", () => {
expect(seen?.has(5)).toBe(false);
await expect(
executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 5.=5:\n+OUTSIDE`, session)),
executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 5-5:\n+OUTSIDE`, session)),
).rejects.toThrow(/never displayed \(it showed/);
expect(await Bun.file(file).text()).toBe(CONTENT);
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 7.=7:\n+RAW RANGE EDITED`, session));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 7-7:\n+RAW RANGE EDITED`, session));
const edited = await Bun.file(file).text();
expect(edited).toContain("line 6\nRAW RANGE EDITED\nline 8");
expect(edited).not.toContain("line 7");
@@ -197,7 +197,7 @@ describe("read → edit seen-line guard", () => {
expect(seen?.has(4)).toBe(true);
await expect(
executeHashlineSingle(execOptions(`[wide-raw.txt#${tag}]\nSWAP 1.=1:\n+REPLACED`, session)),
executeHashlineSingle(execOptions(`[wide-raw.txt#${tag}]\nPUT 1-1:\n+REPLACED`, session)),
).rejects.toThrow(/never displayed \(it showed/);
expect(await Bun.file(file).text()).toBe(content);
});
@@ -214,7 +214,7 @@ describe("read → edit seen-line guard", () => {
const seen = store.byHash(canonicalSnapshotKey(file), tag)?.seenLines;
expect(seen?.has(2)).toBe(true);
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nINS.POST 2:\n+EDITED`, session));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT >2:\n+EDITED`, session));
expect(await Bun.file(file).text()).toContain("line 2\nEDITED");
});
@@ -236,7 +236,7 @@ describe("read → edit seen-line guard", () => {
expect(seen?.has(1)).toBe(true);
expect(seen?.has(2)).toBe(true);
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 1.=1:\n+RAW BLANK EDITED`, session));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 1-1:\n+RAW BLANK EDITED`, session));
expect(await Bun.file(file).text()).toBe("RAW BLANK EDITED");
});
@@ -262,7 +262,7 @@ describe("read → edit seen-line guard", () => {
expect(seen?.has(1122)).toBe(true);
await executeHashlineSingle(
execOptions(
`[src/main.c#${tag}]\nINS.POST 1122:\n+\tbeep_3k8hz_on();\n+\tk_sleep(K_MSEC(300));\n+\tbeep_3k8hz_off();\nCUT 1288.=1291`,
`[src/main.c#${tag}]\nPUT >1122:\n+\tbeep_3k8hz_on();\n+\tk_sleep(K_MSEC(300));\n+\tbeep_3k8hz_off();\nCUT 1288-1291`,
session,
),
);
@@ -281,7 +281,7 @@ describe("read → edit seen-line guard", () => {
let message: string | undefined;
try {
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 10.=12:\n+X10\n+X11\n+X12`, session));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 10-12:\n+X10\n+X11\n+X12`, session));
} catch (err) {
message = (err as Error).message;
}
@@ -296,7 +296,7 @@ describe("read → edit seen-line guard", () => {
// The revealed lines are now in the snapshot's seen set, so a straight
// retry with the same `[path#tag]` header succeeds without a re-read.
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 10.=12:\n+X10\n+X11\n+X12`, session));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 10-12:\n+X10\n+X11\n+X12`, session));
const after = await Bun.file(file).text();
expect(after).toContain("X10\nX11\nX12");
expect(after).not.toContain("line 10");
@@ -348,7 +348,7 @@ describe("read → edit seen-line guard", () => {
const seen = getFileSnapshotStore(session).byHash(canonicalSnapshotKey(file), tag)?.seenLines;
expect(seen?.has(2)).toBe(true);
await executeHashlineSingle(execOptions(`[wide.txt#${tag}]\nSWAP 2.=2:\n+REPLACED`, session));
await executeHashlineSingle(execOptions(`[wide.txt#${tag}]\nPUT 2-2:\n+REPLACED`, session));
expect(await Bun.file(file).text()).toBe("head\nREPLACED\nfoot\n");
});
});
@@ -399,7 +399,7 @@ describe("search → edit seen-line guard", () => {
expect(seen?.has(8)).toBe(false);
// The matched line is in the seen set, so editing it applies.
await executeHashlineSingle(execOptions(`[code.txt#${tag}]\nSWAP 4.=4:\n+NEEDLE edited`, session));
await executeHashlineSingle(execOptions(`[code.txt#${tag}]\nPUT 4-4:\n+NEEDLE edited`, session));
expect(await Bun.file(file).text()).toContain("NEEDLE edited");
});
@@ -412,7 +412,7 @@ describe("search → edit seen-line guard", () => {
const search = await new GrepTool(session).execute("s1", { pattern: "NEEDLE", path: file });
const tag = tagFromOutput(resultText(search));
await expect(executeHashlineSingle(execOptions(`[code.txt#${tag}]\nSWAP 8.=8:\n+X`, session))).rejects.toThrow(
await expect(executeHashlineSingle(execOptions(`[code.txt#${tag}]\nPUT 8-8:\n+X`, session))).rejects.toThrow(
/never displayed \(it showed/,
);
expect(await Bun.file(file).text()).toBe(`${lines.join("\n")}\n`);
@@ -442,7 +442,7 @@ describe("seen-line guard disabled by default", () => {
const tag = tagFromOutput(resultText(read));
// Line 12 was never displayed, but the guard is disabled, so it applies.
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nSWAP 12.=12:\n+EDITED`, session));
await executeHashlineSingle(execOptions(`[notes.txt#${tag}]\nPUT 12-12:\n+EDITED`, session));
expect(await Bun.file(file).text()).toContain("EDITED");
});
});
@@ -31,20 +31,20 @@ describe("EDIT_MODE_STRATEGIES.matcherPaths", () => {
describe("hashline (section-header path)", () => {
it("extracts the path from a single section header", () => {
const input = "[demo.ts#ABCD]\nSWAP 1.=1:\n+const x = 1;\n";
const input = "[demo.ts#ABCD]\nPUT 1-1:\n+const x = 1;\n";
expect(EDIT_MODE_STRATEGIES.hashline.matcherPaths({ input })).toEqual(["demo.ts"]);
});
it("extracts paths from multiple section headers in order, deduped", () => {
const input = [
"[src/a.ts#ABCD]",
"SWAP 1.=1:",
"PUT 1-1:",
"+const a = 1;",
"[src/b.ts#EF01]",
"SWAP 1.=1:",
"PUT 1-1:",
"+const b = 2;",
"[src/a.ts#1234]",
"SWAP 2.=2:",
"PUT 2-2:",
"+const c = 3;",
"",
].join("\n");
@@ -61,10 +61,10 @@ describe("EDIT_MODE_STRATEGIES.matcherPaths", () => {
it("handles paths with spaces and recovers apply_patch-style header noise", () => {
const input = [
"[dir with spaces/file.ts#1A2B]",
"SWAP 1.=1:",
"PUT 1-1:",
"+after",
"[*** Update File: src/recovered.ts#1A2B]",
"SWAP 1.=1:",
"PUT 1-1:",
"+after",
"",
].join("\n");
@@ -76,7 +76,7 @@ describe("EDIT_MODE_STRATEGIES.matcherPaths", () => {
it("returns undefined when input has no section header", () => {
expect(EDIT_MODE_STRATEGIES.hashline.matcherPaths({ input: "" })).toBeUndefined();
expect(EDIT_MODE_STRATEGIES.hashline.matcherPaths({ input: "SWAP 1.=1:\n+x" })).toBeUndefined();
expect(EDIT_MODE_STRATEGIES.hashline.matcherPaths({ input: "PUT 1-1:\n+x" })).toBeUndefined();
});
});
@@ -122,13 +122,13 @@ describe("EDIT_MODE_STRATEGIES.matcherEntries", () => {
it("hashline splits multi-section payloads into one entry per file", () => {
const input = [
"[src/a.ts#ABCD]",
"SWAP 1.=1:",
"PUT 1-1:",
"+const a = 1;",
"[README.md#EF01]",
"SWAP 1.=1:",
"PUT 1-1:",
"+# Heading",
"[src/a.ts#1234]",
"SWAP 2.=2:",
"PUT 2-2:",
"+const c = 3;",
"",
].join("\n");
@@ -198,7 +198,7 @@ describe("hashline edit + path-scoped TTSR (regression: #3646)", () => {
const ANY = "any";
// Snippet rendered at runtime to avoid tripping the rule on this test file itself.
const VIOLATING_LINE = `export const value: ${ANY} = 1;`;
const HASHLINE_PAYLOAD = `[demo.ts#ABCD]\nSWAP 1.=1:\n+${VIOLATING_LINE}\n`;
const HASHLINE_PAYLOAD = `[demo.ts#ABCD]\nPUT 1-1:\n+${VIOLATING_LINE}\n`;
async function makeManager(): Promise<TtsrManager> {
const manager = new TtsrManager({
@@ -250,10 +250,10 @@ describe("hashline edit + path-scoped TTSR (regression: #3646)", () => {
const manager = await makeManager();
const input = [
"[README.md#ABCD]",
"SWAP 1.=1:",
"PUT 1-1:",
`+${VIOLATING_LINE}`,
"[src/ok.ts#EF01]",
"SWAP 1.=1:",
"PUT 1-1:",
"+export const ok = 1;",
"",
].join("\n");
@@ -278,10 +278,10 @@ describe("hashline edit + path-scoped TTSR (regression: #3646)", () => {
const manager = await makeManager();
const input = [
"[README.md#ABCD]",
"SWAP 1.=1:",
"PUT 1-1:",
"+# Heading",
"[src/bad.ts#EF01]",
"SWAP 1.=1:",
"PUT 1-1:",
`+${VIOLATING_LINE}`,
"",
].join("\n");