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.");
|
void vscode.window.showWarningMessage("Cowriting: open a Markdown document to reject its proposals.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { reverted } = await proposalController.rejectAll(doc);
|
const { reverted, skipped } = await proposalController.rejectAll(doc);
|
||||||
if (reverted > 0) {
|
if (reverted === 0 && skipped === 0) return;
|
||||||
void vscode.window.showInformationMessage(
|
const skipNote = skipped > 0 ? `, ${skipped} skipped (applied text not locatable — undo your edits or Keep)` : "";
|
||||||
`Cowriting: rejected ${reverted} proposal${reverted === 1 ? "" : "s"}.`,
|
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
|
* 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
|
* DESCENDING span order (so an earlier revert never shifts a later one's
|
||||||
* offsets), symmetric with #46's accept-all. Returns the reverted count.
|
* 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 }> {
|
async rejectAll(document: vscode.TextDocument): Promise<{ reverted: number; skipped: number }> {
|
||||||
if (!this.isTracked(document)) return { reverted: 0 };
|
if (!this.isTracked(document)) return { reverted: 0, skipped: 0 };
|
||||||
const docPath = this.keyOf(document);
|
const docPath = this.keyOf(document);
|
||||||
const state = this.ensureState(document);
|
const state = this.ensureState(document);
|
||||||
state.artifact = this.store.load(docPath) ?? emptyArtifact(docPath);
|
state.artifact = this.store.load(docPath) ?? emptyArtifact(docPath);
|
||||||
@@ -503,12 +506,17 @@ export class ProposalController implements vscode.Disposable {
|
|||||||
.map((p) => {
|
.map((p) => {
|
||||||
const fp = state.artifact.anchors[p.anchorId]?.fingerprint;
|
const fp = state.artifact.anchors[p.anchorId]?.fingerprint;
|
||||||
const r = fp ? resolve(text, fp) : "orphaned";
|
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);
|
.sort((a, b) => b.start - a.start);
|
||||||
let reverted = 0;
|
let reverted = 0;
|
||||||
for (const it of ordered) if (await this.revertInPlace(docPath, it.id)) reverted++;
|
let skipped = 0;
|
||||||
return { reverted };
|
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 {
|
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.
|
// plain record-only reject.
|
||||||
assert.strictEqual(p.rejectById(docKey, id!), true);
|
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
|
// Reload-safety: a proposal that was optimistically applied in a PRIOR session
|
||||||
|
|||||||
Reference in New Issue
Block a user