e389406637
- shiftTracked (pure, anchorer.ts + unit tests): interior/outside edits shift, boundary-straddling edits distrust; an insertion exactly at the span start lands BEFORE the span (never absorbed into the revert target) - clear tracked spans on doc close (a closed buffer can change on disk with no change events — a kept span could revert over unrelated text, INV-11) - renderAll only REBUILDS an absent span from an exact resolve; a present entry is continuously-tracked ground truth (duplicate-text resolve can't clobber it) - revertInPlace/finalizeInPlace gain the guard.isReadOnly check (INV-16) - the hard-fail warning offers 'Discard proposal (leave text)' so a reloaded session can still dismiss an unlocatable proposal record - ✓/✗ CodeLens pair anchors at the tracked span when a tweak orphans the exact anchor (the fixed reject path stays reachable from the primary gesture) - QuickPick batch menu routes through the reporting accept-all/reject-all commands (a discarded skip tally re-created the silent failure) - one clearProposal helper replaces the thrice-copied clear sequence; reject() now cleans applied/appliedSpans too Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
121 lines
5.3 KiB
TypeScript
121 lines
5.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { buildFingerprint, resolve, shift, shiftTracked, type OffsetRange } from "../src/anchorer";
|
|
|
|
const DOC = "line0\nline1 needle here\nline2\nneedle again on line3\n";
|
|
|
|
describe("buildFingerprint", () => {
|
|
it("captures the exact text, bounded context, and 0-based lineHint", () => {
|
|
const start = DOC.indexOf("needle here");
|
|
const range: OffsetRange = { start, end: start + "needle".length };
|
|
const fp = buildFingerprint(DOC, range);
|
|
expect(fp.text).toBe("needle");
|
|
expect(fp.lineHint).toBe(1);
|
|
expect(fp.before.endsWith("line1 ")).toBe(true);
|
|
expect(fp.after.startsWith(" here")).toBe(true);
|
|
expect(fp.before.length).toBeLessThanOrEqual(120);
|
|
expect(fp.after.length).toBeLessThanOrEqual(120);
|
|
});
|
|
});
|
|
|
|
describe("resolve", () => {
|
|
it("exact-unique: returns the single occurrence's range", () => {
|
|
const fp = { text: "line2", before: "", after: "", lineHint: 0 };
|
|
const start = DOC.indexOf("line2");
|
|
expect(resolve(DOC, fp)).toEqual({ start, end: start + 5 });
|
|
});
|
|
|
|
it("context-disambiguated: picks the occurrence whose before/after match", () => {
|
|
const fp = { text: "needle", before: "line1 ", after: " here", lineHint: 99 };
|
|
const start = DOC.indexOf("needle here");
|
|
expect(resolve(DOC, fp)).toEqual({ start, end: start + 6 });
|
|
});
|
|
|
|
it("lineHint tiebreak: when context does not disambiguate, the closest line wins", () => {
|
|
// both occurrences of 'needle', no usable context, hint points at line 3
|
|
const fp = { text: "needle", before: "", after: "", lineHint: 3 };
|
|
const start = DOC.indexOf("needle again");
|
|
expect(resolve(DOC, fp)).toEqual({ start, end: start + 6 });
|
|
});
|
|
|
|
it("orphaned: returns 'orphaned' when the text is gone", () => {
|
|
const fp = { text: "absent-text", before: "", after: "", lineHint: 0 };
|
|
expect(resolve(DOC, fp)).toBe("orphaned");
|
|
});
|
|
|
|
it("orphaned: returns 'orphaned' rather than guessing on an unbreakable tie (INV-1)", () => {
|
|
const doc = "needle\n....\nneedle\n"; // two identical occurrences, equidistant from hint
|
|
const fp = { text: "needle", before: "", after: "", lineHint: 1 };
|
|
expect(resolve(doc, fp)).toBe("orphaned");
|
|
});
|
|
});
|
|
|
|
describe("shift", () => {
|
|
it("edit entirely before the range shifts both endpoints by the delta", () => {
|
|
const r: OffsetRange = { start: 10, end: 15 };
|
|
// insert 2 chars at offset 0 (replace [0,0) with len 2)
|
|
expect(shift(r, { start: 0, end: 0, newLength: 2 })).toEqual({ start: 12, end: 17 });
|
|
});
|
|
|
|
it("edit entirely after the range leaves it unchanged", () => {
|
|
const r: OffsetRange = { start: 2, end: 5 };
|
|
expect(shift(r, { start: 10, end: 12, newLength: 0 })).toEqual({ start: 2, end: 5 });
|
|
});
|
|
|
|
it("edit overlapping the range clamps the touched endpoints to the edit start", () => {
|
|
const r: OffsetRange = { start: 5, end: 10 };
|
|
// replace [3,7) with 1 char (delta = -3)
|
|
expect(shift(r, { start: 3, end: 7, newLength: 1 })).toEqual({ start: 3, end: 7 });
|
|
});
|
|
});
|
|
|
|
describe("shiftTracked (#70 — tracked applied spans)", () => {
|
|
const r: OffsetRange = { start: 10, end: 20 };
|
|
|
|
it("edit entirely before the span shifts both endpoints by the delta", () => {
|
|
expect(shiftTracked(r, { start: 0, end: 0, newLength: 3 })).toEqual({ start: 13, end: 23 });
|
|
});
|
|
|
|
it("edit entirely after the span leaves it unchanged", () => {
|
|
expect(shiftTracked(r, { start: 25, end: 28, newLength: 0 })).toEqual({ start: 10, end: 20 });
|
|
});
|
|
|
|
it("edit fully inside the span keeps the start and grows/shrinks the end", () => {
|
|
// replace [12,15) with 5 chars (delta = +2)
|
|
expect(shiftTracked(r, { start: 12, end: 15, newLength: 5 })).toEqual({ start: 10, end: 22 });
|
|
});
|
|
|
|
it("replacing the span's exact full contents is an interior edit", () => {
|
|
expect(shiftTracked(r, { start: 10, end: 20, newLength: 4 })).toEqual({ start: 10, end: 14 });
|
|
});
|
|
|
|
it("an insertion exactly at the span start lands BEFORE the span (never absorbed)", () => {
|
|
expect(shiftTracked(r, { start: 10, end: 10, newLength: 6 })).toEqual({ start: 16, end: 26 });
|
|
});
|
|
|
|
it("an insertion exactly at the span end lands AFTER the span (never absorbed)", () => {
|
|
expect(shiftTracked(r, { start: 20, end: 20, newLength: 6 })).toEqual({ start: 10, end: 20 });
|
|
});
|
|
|
|
it("an insertion at an EMPTY span's point lands before it (deletion-proposal span)", () => {
|
|
const empty: OffsetRange = { start: 10, end: 10 };
|
|
expect(shiftTracked(empty, { start: 10, end: 10, newLength: 3 })).toEqual({ start: 13, end: 13 });
|
|
});
|
|
|
|
it("an edit straddling the span's start boundary is distrusted (INV-11)", () => {
|
|
expect(shiftTracked(r, { start: 8, end: 12, newLength: 0 })).toBe("distrusted");
|
|
});
|
|
|
|
it("an edit straddling the span's end boundary is distrusted (INV-11)", () => {
|
|
expect(shiftTracked(r, { start: 18, end: 22, newLength: 1 })).toBe("distrusted");
|
|
});
|
|
|
|
it("an edit fully containing the span (e.g. a wholesale replace) is distrusted (INV-11)", () => {
|
|
expect(shiftTracked(r, { start: 0, end: 30, newLength: 12 })).toBe("distrusted");
|
|
});
|
|
|
|
it("a deletion consuming an EMPTY span's neighborhood is distrusted (INV-11)", () => {
|
|
const empty: OffsetRange = { start: 10, end: 10 };
|
|
expect(shiftTracked(empty, { start: 8, end: 12, newLength: 0 })).toBe("distrusted");
|
|
});
|
|
});
|