From 48535be9700485df128c8a568fc9f85b5b097a32 Mon Sep 17 00:00:00 2001 From: Zach Berry <1112351+zberry@users.noreply.github.com> Date: Sun, 18 Oct 2026 23:58:58 -0500 Subject: [PATCH] [JJ1] review.compare says which side each change is on Each change now carries number (the editor's slide number beside the 0-based slide) and onlyIn (deck or file; absent for a change in both). Shape details say (only in the deck) / (only in the file). The docs say review.accept takes the file's side. PowerPoint: Compare marks the other file's differences as revisions that Accept applies and Reject discards, so accepting takes the file's side; kept. Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/review.rs | 14 +++++++--- crates/engine/src/compare.rs | 48 +++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 82bb3ac6..e9801232 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -58,9 +58,17 @@ pub fn specs() -> Vec { has_text, insert_synonym ), - cmd!(noundo "review.compare", "Compare", ["Review", "Compare"], None, "{path} | {name, data: base64} — compare with a revised copy → {name, changes}", has_doc, compare), - cmd!(query "review.revisions", "Revisions", [], None, "{} → {name, changes: [{kind: inserted|deleted|changed, slide, revised, details}]}", has_doc, revisions), - cmd!("review.accept", "Accept Revision", ["Review", "Compare"], None, "{index} | {all: true}", comparing, accept), + cmd!(noundo "review.compare", "Compare", ["Review", "Compare"], None, "{path} | {name, data: base64} — compare the open deck with another file → {name, changes: [{kind, slide, number, onlyIn?, revised, details}]}; each change is described from the open deck: `onlyIn: \"deck\"` is in the deck and not in the file, `onlyIn: \"file\"` is in the file and not in the deck, `changed` is in both and differs; `slide` is 0-based, `number` the editor's slide number; review.accept takes the FILE's side, so it deletes what is only in the deck (Accept All keeps the deck's last remaining slide rather than deleting it)", has_doc, compare), + cmd!(query "review.revisions", "Revisions", [], None, "{} → {name, changes: [{kind: inserted|deleted|changed, slide (0-based), number (editor's), onlyIn?: deck|file, revised, details}]}; `onlyIn: deck` = only in the open deck, `file` = only in the compared file; accepting takes the file's side", has_doc, revisions), + cmd!( + "review.accept", + "Accept Revision", + ["Review", "Compare"], + None, + "{index} | {all: true} — accept the compared file's side of a change (or all): what is only in the file is added, what is only in the open deck is deleted (all: true leaves the deck's last remaining slide in place), a changed slide becomes the file's", + comparing, + accept + ), cmd!( noundo "review.reject", "Reject Revision", diff --git a/crates/engine/src/compare.rs b/crates/engine/src/compare.rs index fa71dc1a..ecdcea78 100644 --- a/crates/engine/src/compare.rs +++ b/crates/engine/src/compare.rs @@ -92,6 +92,12 @@ pub struct Change { pub kind: ChangeKind, /// Index in the open deck (for inserted: where it would go). pub slide: usize, + /// The editor's number of that slide (`slide` + 1). + pub number: usize, + /// Which side has it alone: `deck` (the open presentation; accepting deletes it) or `file` + /// (the compared file; accepting adds it). Absent when both have it and differ. + #[serde(skip_serializing_if = "Option::is_none")] + pub only_in: Option<&'static str>, /// Index in the revised deck (none for deleted). pub revised: Option, /// What differs, readable: `Title 1: text`, `Picture 3 inserted`, `notes`… @@ -118,19 +124,33 @@ pub fn diff(cur: &Presentation, rev: &Presentation, by_position: bool) -> Vec shape_details(cp, sa, rp, sb, &mut out); } } - _ => out.push(format!("{} deleted", shape_label(sa))), + _ => out.push(format!("{} deleted (only in the deck)", shape_label(sa))), } } for (k, sb) in b.shapes.iter().enumerate() { if !used.contains(&k) { - out.push(format!("{} inserted", shape_label(sb))); + out.push(format!("{} inserted (only in the file)", shape_label(sb))); } } let order = |s: &Slide| s.shapes.iter().map(|x| x.name.clone()).collect::>(); @@ -494,6 +514,24 @@ mod tests { assert_eq!(bytes(back.media), b"open background"); } + #[test] + fn each_change_says_which_side_it_is_on() { + let (mut s, rev, _) = decks(); + let v = s.execute("review.compare", &json!({"name": "rev.deckcraft", "data": rev})).unwrap(); + for c in v["changes"].as_array().unwrap() { + assert_eq!(c["number"].as_u64().unwrap(), c["slide"].as_u64().unwrap() + 1, "{c}"); + match c["kind"].as_str().unwrap() { + "deleted" => assert_eq!(c["onlyIn"], "deck", "{c}"), + "inserted" => assert_eq!(c["onlyIn"], "file", "{c}"), + _ => assert!(c.get("onlyIn").is_none(), "{c}"), + } + } + // A shape only in the open deck reads as only in the deck. + s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [10.0, 10.0, 40.0, 40.0]})).unwrap(); + let v = s.execute("review.revisions", &json!({})).unwrap(); + assert!(v.to_string().contains("deleted (only in the deck)"), "{v}"); + } + #[test] fn accept_one_then_all_makes_the_decks_equal_and_undoes() { let (mut s, rev, n) = decks();