mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-30 21:47:00 -06:00
[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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
55e5ee56b4
commit
48535be970
@@ -58,9 +58,17 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
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",
|
||||
|
||||
@@ -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<usize>,
|
||||
/// 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<Ch
|
||||
let (Some(a), Some(b)) = (cur.slides.get(*i), rev.slides.get(*j)) else { continue };
|
||||
let details = slide_details(cur, a, rev, b);
|
||||
if !details.is_empty() {
|
||||
out.push(Change { kind: ChangeKind::Changed, slide: *i, revised: Some(*j), details });
|
||||
out.push(Change { kind: ChangeKind::Changed, slide: *i, number: i.saturating_add(1), only_in: None, revised: Some(*j), details });
|
||||
}
|
||||
}
|
||||
for (i, s) in cur.slides.iter().enumerate() {
|
||||
if !cur_matched.contains(&i) {
|
||||
out.push(Change { kind: ChangeKind::Deleted, slide: i, revised: None, details: vec![slide_title(s)] });
|
||||
out.push(Change {
|
||||
kind: ChangeKind::Deleted,
|
||||
slide: i,
|
||||
number: i.saturating_add(1),
|
||||
only_in: Some("deck"),
|
||||
revised: None,
|
||||
details: vec![slide_title(s)],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (j, s) in rev.slides.iter().enumerate() {
|
||||
if !rev_matched.contains(&j) {
|
||||
// After the open slide that matches the revised slide before it.
|
||||
let at = (0..j).rev().find_map(|k| pairs.iter().find(|p| p.1 == k).map(|p| p.0 + 1)).unwrap_or(0);
|
||||
out.push(Change { kind: ChangeKind::Inserted, slide: at, revised: Some(j), details: vec![slide_title(s)] });
|
||||
out.push(Change {
|
||||
kind: ChangeKind::Inserted,
|
||||
slide: at,
|
||||
number: at.saturating_add(1),
|
||||
only_in: Some("file"),
|
||||
revised: Some(j),
|
||||
details: vec![slide_title(s)],
|
||||
});
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|c| (c.slide, c.revised.unwrap_or(0)));
|
||||
@@ -230,12 +250,12 @@ fn slide_details(cp: &Presentation, a: &Slide, rp: &Presentation, b: &Slide) ->
|
||||
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::<Vec<_>>();
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user