fix(#70): harden the tracked-span revert per branch review
- 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>
This commit is contained in:
@@ -101,6 +101,30 @@ export function resolve(docText: string, fp: Fingerprint): OffsetRange | "orphan
|
||||
return "orphaned";
|
||||
}
|
||||
|
||||
/**
|
||||
* #70 (INV-5/INV-11): maintain a TRACKED applied span across an in-session edit.
|
||||
* Unlike `shift`, which clamps every point into a best-effort range, a tracked
|
||||
* span must stay *provably* meaningful — it is a revert target. So:
|
||||
* - an edit fully OUTSIDE or fully INSIDE the span shifts it (the span is
|
||||
* still "the applied block, as tweaked");
|
||||
* - an insertion exactly AT the span start lands BEFORE the span (both ends
|
||||
* shift right) — matching exact-resolve semantics, where text typed just
|
||||
* before the applied block is never part of it;
|
||||
* - any edit STRADDLING a span boundary returns "distrusted": the shifted
|
||||
* result would be a clamped guess, and stale text is never reverted by
|
||||
* guess (INV-11).
|
||||
*/
|
||||
export function shiftTracked(range: OffsetRange, edit: TextEdit): OffsetRange | "distrusted" {
|
||||
const delta = edit.newLength - (edit.end - edit.start);
|
||||
if (edit.start === edit.end && edit.start === range.start) {
|
||||
return { start: range.start + delta, end: range.end + delta };
|
||||
}
|
||||
const outside = edit.end <= range.start || edit.start >= range.end;
|
||||
const inside = edit.start >= range.start && edit.end <= range.end;
|
||||
if (!outside && !inside) return "distrusted";
|
||||
return shift(range, edit);
|
||||
}
|
||||
|
||||
/** Maintain a live range across an in-session edit (spec §6.4 `shift`). */
|
||||
export function shift(range: OffsetRange, edit: TextEdit): OffsetRange {
|
||||
const delta = edit.newLength - (edit.end - edit.start);
|
||||
|
||||
Reference in New Issue
Block a user