feat(f11): SLICE-3 — Edit Document button + per-hunk proposal path (#43, INV-37)
A whole-document Ask-Claude rewrite is diffed into hunks and surfaced as N independent F4 proposals (one per changed hunk) — reusing the F4 single-range model N times, no new model. Per spec §6.4/§7.2 SLICE-3. - trackChangesModel: pure `diffToHunks(currentText, rewrittenText)` → EditHunk[] (vscode-free, deterministic; diffWordsWithSpace, coalescing adjacent add/remove runs; offsets index currentText). - trackChangesPreview: `runEditAndPropose(document, target, instruction)` — the shared host routine (selection → one single-range propose; document → diff → one propose per hunk; never mutates the doc, INV-10); `askClaude` UI wrapper (host showInputBox keeps LLM/secrets out of the sealed webview, INV-8/35); injectable `editTurn` + `setEditTurnForTest` seam (no LLM in CI); the `askClaude` inbound message branch; `cowriting.editDocument` command for #42 reuse. - package.json: register cowriting.editDocument, palette-guarded on markdown. - webview: ✦ Ask Claude to Edit Document button → { askClaude, scope:"document" }. - unit: diffToHunks fixtures (zero/one/multi-hunk, wholesale, determinism). - host E2E: stubbed multi-hunk rewrite → N matching proposals, doc untouched; editDocument command registered + markdown-guarded. 205 unit + 49 host E2E green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, it, test, expect } from "vitest";
|
||||
import { splitBlocks, splitBlocksWithRanges, diffBlocks, renderTrackChanges, colorByAuthor, type AuthorSpan } from "../src/trackChangesModel";
|
||||
import { splitBlocks, splitBlocksWithRanges, diffBlocks, diffToHunks, renderTrackChanges, colorByAuthor, type AuthorSpan } from "../src/trackChangesModel";
|
||||
|
||||
describe("splitBlocks", () => {
|
||||
it("splits prose paragraphs on blank lines, dropping empties", () => {
|
||||
@@ -320,6 +320,48 @@ describe("renderTrackChanges — intra-diagram mermaid (#22)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// F11 SLICE-3 (INV-37, §6.4): a whole-document rewrite is diffed into per-hunk
|
||||
// proposal ranges — each an independent F4 single-range proposal. Pure,
|
||||
// vscode-free, deterministic; offsets index into currentText.
|
||||
describe("F11 diffToHunks (INV-37)", () => {
|
||||
test("an identical rewrite → zero hunks", () => {
|
||||
expect(diffToHunks("The same text.\n", "The same text.\n")).toEqual([]);
|
||||
});
|
||||
|
||||
test("a single changed word → one hunk over exactly that word", () => {
|
||||
const current = "The quick brown fox.";
|
||||
const hunks = diffToHunks(current, "The quick red fox.");
|
||||
expect(hunks).toHaveLength(1);
|
||||
expect(current.slice(hunks[0].start, hunks[0].end)).toBe("brown");
|
||||
expect(hunks[0].replacement).toBe("red");
|
||||
});
|
||||
|
||||
test("two disjoint changes → two hunks with correct ranges + replacements", () => {
|
||||
const current = "one two three four";
|
||||
const hunks = diffToHunks(current, "one TWO three FOUR");
|
||||
expect(hunks).toHaveLength(2);
|
||||
expect(current.slice(hunks[0].start, hunks[0].end)).toBe("two");
|
||||
expect(hunks[0].replacement).toBe("TWO");
|
||||
expect(current.slice(hunks[1].start, hunks[1].end)).toBe("four");
|
||||
expect(hunks[1].replacement).toBe("FOUR");
|
||||
// disjoint + ordered
|
||||
expect(hunks[0].end).toBeLessThanOrEqual(hunks[1].start);
|
||||
});
|
||||
|
||||
test("a wholesale replacement (nothing in common) → one full-range hunk", () => {
|
||||
const current = "alpha";
|
||||
const hunks = diffToHunks(current, "omega");
|
||||
expect(hunks).toHaveLength(1);
|
||||
expect(hunks[0]).toEqual({ start: 0, end: current.length, replacement: "omega" });
|
||||
});
|
||||
|
||||
test("is deterministic — same inputs → identical hunks", () => {
|
||||
const a = diffToHunks("a b c d", "a B c D");
|
||||
const b = diffToHunks("a b c d", "a B c D");
|
||||
expect(a).toEqual(b);
|
||||
});
|
||||
});
|
||||
|
||||
// F11 SLICE-2 (INV-36): the pure render layer emits data-src-start/data-src-end
|
||||
// (source char offsets from BlockWithRange) on every LIVE-source rendered block,
|
||||
// in BOTH modes. The webview's selection→source mapping walks the DOM to the
|
||||
|
||||
Reference in New Issue
Block a user