From d8aff8bfc7feb52f80cf40fd1d71bf8fc425dd4d Mon Sep 17 00:00:00 2001 From: nleva Date: Fri, 23 Oct 2026 17:09:29 +0200 Subject: [PATCH] M7: a batch of content edits refreshes the added content and page marks A content-only edit re-reads Document::added() and marks() only when the edit itself is one that changes them. Edit::Batch has Scope::Content when all its parts do, but content_refresh_lists never looked inside it, so a batch of AddText/DeleteContent or AddWatermark edits left both lists stale until the next full refresh or undo. It now answers for its parts. Co-Authored-By: Opus 5.5 medium ultracode Claude-Session: https://claude.ai/code/session_017gEgu9NcTcPqKovsLyfPxK --- crates/engine/src/lib.rs | 7 +++++++ crates/engine/src/tests.rs | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 68fc076b9..14ec5991f 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -4177,6 +4177,13 @@ impl Session { /// Which derived lists a content-only edit can change: added content (`None`, from /// undo/redo, re-reads both: always correct, rarely paid). fn content_refresh_lists(edit: Option<&Edit>) -> (bool, bool) { + if let Some(Edit::Batch { edits, .. }) = edit { + // A batch of content edits (from a script, say) changes what its parts change. + return edits.iter().fold((false, false), |(added, marks), part| { + let (a, m) = Self::content_refresh_lists(Some(part)); + (added || a, marks || m) + }); + } let touches_added = edit.is_none_or(|e| { matches!( e, diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index ed4e6efa1..ce3cf14a8 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -3155,6 +3155,21 @@ fn added_text_and_images_render_and_stay_editable() { assert_eq!(s.get(id).unwrap().added().len(), 2); } +#[test] +fn a_batch_of_content_edits_refreshes_the_added_content_and_marks() { + let (mut s, id) = session_with(1); + let text = |t: &str, y: f64| AddedText { rect: [20.0, y, 180.0, y + 14.4], text: t.into(), ..AddedText::default() }; + let edits = vec![Edit::AddText { page: 0, text: text("One", 200.0) }, Edit::AddText { page: 0, text: text("Two", 150.0) }]; + s.apply(id, Edit::Batch { label: "Script".into(), edits }).unwrap(); + assert_eq!(s.get(id).unwrap().added().len(), 2); + s.apply(id, Edit::Batch { label: "Script".into(), edits: vec![Edit::DeleteContent { page: 0, index: 0 }] }).unwrap(); + assert_eq!(s.get(id).unwrap().added().len(), 1); + let watermark = + Edit::AddWatermark { pages: vec![0], settings: Watermark { text: "Draft".into(), ..Watermark::default() }, file: None, replace: false }; + s.apply(id, Edit::Batch { label: "Script".into(), edits: vec![watermark] }).unwrap(); + assert_eq!(s.get(id).unwrap().marks().len(), 1); +} + #[test] fn added_images_rotate_flip_and_crop_as_drawn() { // A 20×10 image: left half red, right half blue.