feat(render): pure decorationPlan feeds editor + webview from one diff (#64, INV-49)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -263,6 +263,42 @@ export function wordEditHunks(currentText: string, rewrittenText: string): EditH
|
||||
return hunks;
|
||||
}
|
||||
|
||||
/** F12/#64 (INV-49/52): the editor render of one optimistically-applied proposal. */
|
||||
export interface DecorationPlan {
|
||||
/** buffer ranges of inserted (proposed) text → tinted (INV-52). */
|
||||
insertions: { start: number; end: number }[];
|
||||
/** struck original text shown as a non-editable hint at a buffer offset (INV-52). */
|
||||
deletions: { at: number; text: string }[];
|
||||
}
|
||||
|
||||
/**
|
||||
* F12/#64 (INV-49): compute the editor decoration plan for a proposal whose applied
|
||||
* text occupies `[anchorStart, anchorStart+replacement.length)` in the buffer. The
|
||||
* SAME word diff the webview uses (`wordEditHunks`) drives it, so both surfaces show
|
||||
* the identical diff. A changed run maps to (a) an insertion range over the run's
|
||||
* applied text and (b) a deletion hint carrying the run's removed original text at
|
||||
* the run start; a pure insertion has no deletion hint; a pure deletion has only a
|
||||
* hint. Pure, vscode-free, deterministic.
|
||||
*/
|
||||
export function decorationPlan(anchorStart: number, original: string, replacement: string): DecorationPlan {
|
||||
const insertions: { start: number; end: number }[] = [];
|
||||
const deletions: { at: number; text: string }[] = [];
|
||||
// Walk the word diff once, tracking the applied-side offset as we consume parts.
|
||||
let appliedOffset = anchorStart;
|
||||
for (const part of diffWordsWithSpace(original, replacement)) {
|
||||
if (part.added) {
|
||||
insertions.push({ start: appliedOffset, end: appliedOffset + part.value.length });
|
||||
appliedOffset += part.value.length;
|
||||
} else if (part.removed) {
|
||||
deletions.push({ at: appliedOffset, text: part.value });
|
||||
// removed text is NOT in the applied buffer → appliedOffset does not advance
|
||||
} else {
|
||||
appliedOffset += part.value.length;
|
||||
}
|
||||
}
|
||||
return { insertions, deletions };
|
||||
}
|
||||
|
||||
const isWs = (c: string): boolean => /\s/.test(c);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { decorationPlan } from "../src/trackChangesModel";
|
||||
|
||||
describe("decorationPlan (INV-49)", () => {
|
||||
test("derives insertion ranges + deletion hints from original→replacement", () => {
|
||||
// anchorStart 10; original 'the brown fox' → 'the red fox'
|
||||
const plan = decorationPlan(10, "the brown fox", "the red fox");
|
||||
// one changed run: 'brown ' → 'red ' (word-level); insertion 'red ' tinted,
|
||||
// deletion hint 'brown ' shown at the run start.
|
||||
expect(plan.insertions.length).toBeGreaterThan(0);
|
||||
expect(plan.deletions.length).toBeGreaterThan(0);
|
||||
// insertion offsets are in buffer coords (>= anchorStart) and lie within the applied text
|
||||
for (const ins of plan.insertions) {
|
||||
expect(ins.start).toBeGreaterThanOrEqual(10);
|
||||
expect(ins.end).toBeLessThanOrEqual(10 + "the red fox".length);
|
||||
}
|
||||
// a deletion hint carries the struck original text at a buffer offset
|
||||
expect(plan.deletions[0].text).toContain("brown");
|
||||
});
|
||||
|
||||
test("pure insertion (no deletion) yields an insertion range and no deletion hint", () => {
|
||||
// 'hello' → 'hello world': ' world' is a pure word-level insertion (no removed tokens)
|
||||
const plan = decorationPlan(0, "hello", "hello world");
|
||||
expect(plan.insertions.length).toBe(1);
|
||||
expect(plan.deletions.length).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user