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;
|
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);
|
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