fix(#70): rejectAll reverts tweaked proposals via the tracked span; skips are counted and reported
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+6
-6
@@ -223,12 +223,12 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
void vscode.window.showWarningMessage("Cowriting: open a Markdown document to reject its proposals.");
|
||||
return;
|
||||
}
|
||||
const { reverted } = await proposalController.rejectAll(doc);
|
||||
if (reverted > 0) {
|
||||
void vscode.window.showInformationMessage(
|
||||
`Cowriting: rejected ${reverted} proposal${reverted === 1 ? "" : "s"}.`,
|
||||
);
|
||||
}
|
||||
const { reverted, skipped } = await proposalController.rejectAll(doc);
|
||||
if (reverted === 0 && skipped === 0) return;
|
||||
const skipNote = skipped > 0 ? `, ${skipped} skipped (applied text not locatable — undo your edits or Keep)` : "";
|
||||
void vscode.window.showInformationMessage(
|
||||
`Cowriting: rejected ${reverted} proposal${reverted === 1 ? "" : "s"}${skipNote}.`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -490,11 +490,14 @@ export class ProposalController implements vscode.Disposable {
|
||||
|
||||
/**
|
||||
* F12/#64 (INV-53): reject EVERY pending proposal on a document — revert each in
|
||||
* DESCENDING anchor order (so an earlier revert never shifts a later one's
|
||||
* offsets), symmetric with #46's accept-all. Returns the reverted count.
|
||||
* DESCENDING span order (so an earlier revert never shifts a later one's
|
||||
* offsets), symmetric with #46's accept-all. #70: a tweaked proposal whose exact
|
||||
* anchor is orphaned orders (and reverts) by its shift-tracked applied span;
|
||||
* one with no locatable span is SKIPPED — counted, never guessed (INV-11) —
|
||||
* and the per-proposal warning is suppressed in favor of the batch report.
|
||||
*/
|
||||
async rejectAll(document: vscode.TextDocument): Promise<{ reverted: number }> {
|
||||
if (!this.isTracked(document)) return { reverted: 0 };
|
||||
async rejectAll(document: vscode.TextDocument): Promise<{ reverted: number; skipped: number }> {
|
||||
if (!this.isTracked(document)) return { reverted: 0, skipped: 0 };
|
||||
const docPath = this.keyOf(document);
|
||||
const state = this.ensureState(document);
|
||||
state.artifact = this.store.load(docPath) ?? emptyArtifact(docPath);
|
||||
@@ -503,12 +506,17 @@ export class ProposalController implements vscode.Disposable {
|
||||
.map((p) => {
|
||||
const fp = state.artifact.anchors[p.anchorId]?.fingerprint;
|
||||
const r = fp ? resolve(text, fp) : "orphaned";
|
||||
return { id: p.id, start: r === "orphaned" ? -1 : r.start };
|
||||
const start = r !== "orphaned" ? r.start : state.appliedSpans.get(p.id)?.start ?? -1;
|
||||
return { id: p.id, start };
|
||||
})
|
||||
.sort((a, b) => b.start - a.start);
|
||||
let reverted = 0;
|
||||
for (const it of ordered) if (await this.revertInPlace(docPath, it.id)) reverted++;
|
||||
return { reverted };
|
||||
let skipped = 0;
|
||||
for (const it of ordered) {
|
||||
if (await this.revertInPlace(docPath, it.id, { silent: true })) reverted++;
|
||||
else skipped++;
|
||||
}
|
||||
return { reverted, skipped };
|
||||
}
|
||||
|
||||
private reject(state: DocState, proposal: Proposal): void {
|
||||
|
||||
@@ -293,6 +293,33 @@ suite("F12 inline diff — #70 reject after interior edit (INV-5)", () => {
|
||||
// plain record-only reject.
|
||||
assert.strictEqual(p.rejectById(docKey, id!), true);
|
||||
});
|
||||
|
||||
// rejectAll must revert a tweaked (orphaned-anchor) proposal via the same
|
||||
// tracked-span fallback, ordered descending by that span so earlier reverts
|
||||
// never shift later ones — and must count what it could not revert.
|
||||
test("rejectAll reverts tweaked proposals via the tracked span and reports skips", async () => {
|
||||
const { doc } = await freshDoc("docs/s70-rejall-tweak.md", "# T\n\nOne aaa.\n\nTwo bbb.\n");
|
||||
const api = await getApi();
|
||||
const p = api.proposalController;
|
||||
const editFlow = api.editFlow;
|
||||
editFlow.setEditTurnForTest(async () => ({ replacement: "# T\n\nOne AAA.\n\nTwo BBB.\n", model: "m", sessionId: "s" }));
|
||||
const ids = await editFlow.runEditAndPropose(doc, { kind: "document" }, "up");
|
||||
await settle();
|
||||
for (const id of ids) await p.optimisticApply(doc, id);
|
||||
await settle();
|
||||
// Tweak INSIDE the first applied block → its exact anchor orphans.
|
||||
const at = doc.getText().indexOf("AAA");
|
||||
const we = new vscode.WorkspaceEdit();
|
||||
we.replace(doc.uri, new vscode.Range(doc.positionAt(at), doc.positionAt(at + 3)), "AAAZ");
|
||||
assert.ok(await vscode.workspace.applyEdit(we), "interior tweak applied");
|
||||
await settle();
|
||||
const { reverted, skipped } = await p.rejectAll(doc);
|
||||
await settle();
|
||||
assert.strictEqual(reverted, ids.length, "ALL proposals reverted, tweaked one included");
|
||||
assert.strictEqual(skipped, 0, "nothing skipped");
|
||||
assert.strictEqual(doc.getText(), "# T\n\nOne aaa.\n\nTwo bbb.\n", "document fully restored (INV-5)");
|
||||
assert.strictEqual(p.listProposals(doc).length, 0, "all cleared");
|
||||
});
|
||||
});
|
||||
|
||||
// Reload-safety: a proposal that was optimistically applied in a PRIOR session
|
||||
|
||||
Reference in New Issue
Block a user