diff --git a/crates/doc/src/edit.rs b/crates/doc/src/edit.rs index 2a83e5e17..3a224d039 100644 --- a/crates/doc/src/edit.rs +++ b/crates/doc/src/edit.rs @@ -311,7 +311,7 @@ impl Document { let st = self.story_mut(fs).ok_or(DocError::NoStory(fs))?; let at = st.frames.iter().position(|f| *f == from).map_or(st.frames.len(), |p| p + 1); st.frames.insert(at, to); - st.rev += 1; + st.rev = st.rev.wrapping_add(1); let it = self.item_mut(to).ok_or(DocError::NoItem(to))?; it.content = Content::Text(TextFrame { story: fs, options: TextFrameOptions::default() }); return Ok(()); @@ -332,7 +332,7 @@ impl Document { for (k, f) in after.iter().enumerate() { st.frames.insert(at + k, *f); } - st.rev += 1; + st.rev = st.rev.wrapping_add(1); } for f in &after { if let Some(t) = self.item_mut(*f).and_then(Item::text_frame_mut) { @@ -369,7 +369,7 @@ impl Document { } let st = self.story_mut(ts).ok_or(DocError::NoStory(ts))?; st.frames.insert(0, from); - st.rev += 1; + st.rev = st.rev.wrapping_add(1); let it = self.item_mut(from).ok_or(DocError::NoItem(from))?; match it.text_frame_mut() { Some(t) => t.story = ts, @@ -438,7 +438,7 @@ impl Document { let st = self.story_mut(sid).ok_or(DocError::NoStory(sid))?; let p = st.frames.iter().position(|f| *f == frame).ok_or(DocError::NoItem(frame))?; let tail = st.frames.split_off(p + 1); - st.rev += 1; + st.rev = st.rev.wrapping_add(1); (tail, st.vertical, st.direction) }; if tail.is_empty() { @@ -468,7 +468,7 @@ impl Document { return Err(DocError::Invalid(format!("frame {frame} is not threaded to another frame"))); } st.frames.retain(|f| *f != frame); - st.rev += 1; + st.rev = st.rev.wrapping_add(1); (st.vertical, st.direction) }; let nid = StoryId(self.alloc()); @@ -572,7 +572,7 @@ impl Document { st.index_refs.clear(); st.objects.clear(); st.fix_marks(); - st.rev += 1; + st.rev = st.rev.wrapping_add(1); Ok(()) } } diff --git a/crates/doc/src/endnotes.rs b/crates/doc/src/endnotes.rs index b700254a4..553ea4d0a 100644 --- a/crates/doc/src/endnotes.rs +++ b/crates/doc/src/endnotes.rs @@ -83,7 +83,7 @@ impl Story { /// Mutable endnote (copy-on-write); bumps the story revision. pub fn endnote_mut(&mut self, id: u64) -> Option<&mut Footnote> { let n = self.endnotes.iter_mut().find(|n| n.id == id)?; - self.rev += 1; + self.rev = self.rev.wrapping_add(1); Some(Arc::make_mut(n)) } @@ -179,6 +179,6 @@ impl crate::Document { let Some(st) = self.stories.get_mut(&sid) else { return }; let st = Arc::make_mut(st); let (frames, rev) = (std::mem::take(&mut st.frames), st.rev); - *st = Story { frames, rev: rev + 1, ..fresh }; + *st = Story { frames, rev: rev.wrapping_add(1), ..fresh }; } } diff --git a/crates/doc/src/notes.rs b/crates/doc/src/notes.rs index e4c85043f..890be70d5 100644 --- a/crates/doc/src/notes.rs +++ b/crates/doc/src/notes.rs @@ -156,7 +156,7 @@ impl Story { /// Mutable footnote (copy-on-write); bumps the story revision. pub fn note_mut(&mut self, id: u64) -> Option<&mut Footnote> { let n = self.notes.iter_mut().find(|n| n.id == id)?; - self.rev += 1; + self.rev = self.rev.wrapping_add(1); Some(Arc::make_mut(n)) } diff --git a/crates/doc/src/slice.rs b/crates/doc/src/slice.rs index 6afbad109..e188276dd 100644 --- a/crates/doc/src/slice.rs +++ b/crates/doc/src/slice.rs @@ -123,7 +123,7 @@ impl Story { } self.fix_tables(); } - self.rev += 1; + self.rev = self.rev.wrapping_add(1); end } diff --git a/crates/doc/src/story.rs b/crates/doc/src/story.rs index d29f6a1ff..aadbc7b5a 100644 --- a/crates/doc/src/story.rs +++ b/crates/doc/src/story.rs @@ -307,7 +307,7 @@ impl Story { // Character runs. self.splice_runs(pos, 0, text.len(), fmt); self.normalize(); - self.rev += 1; + self.rev = self.rev.wrapping_add(1); } /// End the paragraph after every break character that isn't followed by a `\n`. @@ -361,7 +361,7 @@ impl Story { self.remove_run_bytes(a, b - a); self.normalize(); self.fix_tables(); - self.rev += 1; + self.rev = self.rev.wrapping_add(1); } /// Replace a range with text (delete + insert using the format at the start of the range). @@ -402,7 +402,7 @@ impl Story { start = end; } self.normalize(); - self.rev += 1; + self.rev = self.rev.wrapping_add(1); } /// Apply `f` to every paragraph intersecting `range` (an empty range = the paragraph at it). @@ -414,7 +414,7 @@ impl Story { f(p); p.table = table; } - self.rev += 1; + self.rev = self.rev.wrapping_add(1); } /// Plain text of a range. @@ -597,6 +597,31 @@ mod tests { s.check().unwrap(); } + #[test] + fn saved_revision_wraps_without_stopping_edits() { + let mut s = st("seed"); + s.rev = u64::MAX; + // Revisions are serialized; a valid saved story can reach the boundary. + let bytes = serde_json::to_vec(&s).unwrap(); + let mut s: Story = serde_json::from_slice(&bytes).unwrap(); + s.insert(s.len(), " text"); + assert_eq!(s.rev, 0); + assert_eq!(s.text, "seed text"); + s.rev = u64::MAX; + s.delete(4..s.len()); + assert_eq!(s.rev, 0); + assert_eq!(s.text, "seed"); + s.rev = u64::MAX; + s.format_chars(0..s.len(), |f| f.over.size = Some(18.0)); + assert_eq!(s.rev, 0); + assert_eq!(s.char_format_at(0).over.size, Some(18.0)); + s.rev = u64::MAX; + s.format_paras(0..s.len(), |p| p.style = "Heading".into()); + assert_eq!(s.rev, 0); + assert_eq!(s.paras[0].style, "Heading"); + s.check().unwrap(); + } + #[test] fn replace_and_format_paras() { let mut s = st("a\nb\nc"); diff --git a/crates/doc/src/styles.rs b/crates/doc/src/styles.rs index 4699e1af1..8dcb62d86 100644 --- a/crates/doc/src/styles.rs +++ b/crates/doc/src/styles.rs @@ -523,7 +523,7 @@ impl CellStyle { for p in &mut cell.text.paras { p.style = ps.clone(); } - cell.text.rev += 1; + cell.text.rev = cell.text.rev.wrapping_add(1); } cell.style = if self.name == NO_CELL_STYLE { String::new() } else { self.name.clone() }; } diff --git a/crates/doc/src/table.rs b/crates/doc/src/table.rs index 8752075c8..660491ff5 100644 --- a/crates/doc/src/table.rs +++ b/crates/doc/src/table.rs @@ -923,7 +923,7 @@ impl Table { fn append_story(s: &mut Story, other: &Story) { if s.is_empty() { *s = Story { id: s.id, ..other.clone() }; - s.rev += 1; + s.rev = s.rev.wrapping_add(1); return; } let base = s.len(); @@ -1000,7 +1000,7 @@ impl Story { /// Mutable table (copy-on-write); bumps the story revision. pub fn table_mut(&mut self, id: u64) -> Option<&mut Table> { let t = self.tables.get_mut(&id)?; - self.rev += 1; + self.rev = self.rev.wrapping_add(1); Some(Arc::make_mut(t)) } @@ -1025,7 +1025,7 @@ impl Story { let api = self.para_at(anchor); self.paras[api].table = Some(id); self.fix_tables(); - self.rev += 1; + self.rev = self.rev.wrapping_add(1); anchor } diff --git a/crates/engine/src/cmd/anchored.rs b/crates/engine/src/cmd/anchored.rs index 8c85c7062..6c3c202c3 100644 --- a/crates/engine/src/cmd/anchored.rs +++ b/crates/engine/src/cmd/anchored.rs @@ -190,7 +190,7 @@ fn options(s: &mut Session, p: &Value) -> Result { let st = d.story_mut(sid).ok_or(designcraft_doc::DocError::NoStory(sid))?; let o = st.objects.get_mut(k).ok_or_else(|| bad("anchored.options", "no such anchored object"))?; Arc::make_mut(o).position = position.clone(); - st.rev += 1; + st.rev = st.rev.wrapping_add(1); Ok(json!({"position": position})) }) } diff --git a/crates/engine/src/cmd/editorial.rs b/crates/engine/src/cmd/editorial.rs index 6cee420ed..0f1b06615 100644 --- a/crates/engine/src/cmd/editorial.rs +++ b/crates/engine/src/cmd/editorial.rs @@ -47,7 +47,7 @@ pub fn specs() -> Vec { if let Some(a) = author { n.author = a; } - st.rev += 1; + st.rev = st.rev.wrapping_add(1); ok() }) }), diff --git a/crates/engine/src/cmd/linked.rs b/crates/engine/src/cmd/linked.rs index ec07a24b2..85f4fbd5d 100644 --- a/crates/engine/src/cmd/linked.rs +++ b/crates/engine/src/cmd/linked.rs @@ -12,7 +12,7 @@ fn copy_into(d: &mut Document, parent: StoryId, child: StoryId) -> Result<()> { let src = d.story(parent).ok_or_else(|| bad("story.link", format!("no story {}", parent.0)))?.clone(); let st = d.story_mut(child).ok_or_else(|| bad("story.link", format!("no story {}", child.0)))?; let (frames, rev) = (std::mem::take(&mut st.frames), st.rev); - *st = Story { id: child, frames, rev: rev + 1, link: Some((parent, src.rev)), ..src }; + *st = Story { id: child, frames, rev: rev.wrapping_add(1), link: Some((parent, src.rev)), ..src }; Ok(()) } @@ -94,6 +94,26 @@ mod tests { use crate::Session; + #[test] + fn linked_story_update_wraps_the_child_revision() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let p = s.execute("frame.create", &json!({"rect": [0, 0, 100, 100], "content": "text", "text": "Original"})).unwrap(); + let c = s.execute("story.placeAndLink", &json!({"story": p["story"], "rect": [100, 0, 200, 100]})).unwrap(); + let child = designcraft_doc::StoryId(c["story"].as_u64().unwrap()); + std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc).story_mut(child).unwrap().rev = u64::MAX; + s.execute("story.setText", &json!({"story": p["story"], "text": "Revised"})).unwrap(); + s.execute("story.updateLink", &json!({"story": c["story"]})).unwrap(); + let st = s.doc().unwrap().doc.story(child).unwrap(); + assert_eq!(st.text, "Revised"); + assert_eq!(st.rev, 0); + assert_eq!(s.execute("story.links", &json!({})).unwrap()[0]["outOfDate"], false); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().doc.story(child).unwrap().text, "Original"); + s.execute("edit.redo", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().doc.story(child).unwrap().text, "Revised"); + } + #[test] fn linked_story_goes_out_of_date_and_updates() { let mut s = Session::new(); diff --git a/crates/engine/src/cmd/style.rs b/crates/engine/src/cmd/style.rs index 54299d40f..cdfcd14e5 100644 --- a/crates/engine/src/cmd/style.rs +++ b/crates/engine/src/cmd/style.rs @@ -623,7 +623,7 @@ fn rename_style(d: &mut designcraft_doc::Document, para: bool, from: &str, to: & rename_char_refs(&mut f.para, from, to); } } - st.rev += 1; + st.rev = st.rev.wrapping_add(1); }); } } diff --git a/crates/engine/src/cmd/table.rs b/crates/engine/src/cmd/table.rs index 754f0b150..4a0271da7 100644 --- a/crates/engine/src/cmd/table.rs +++ b/crates/engine/src/cmd/table.rs @@ -1363,7 +1363,7 @@ fn reapply_cell_styles(d: &mut Document, names: &[String]) { } } } - st.rev += 1; + st.rev = st.rev.wrapping_add(1); } } @@ -1488,7 +1488,7 @@ fn table_style_edit(s: &mut Session, p: &Value) -> Result { ts.apply_to(std::sync::Arc::make_mut(t), &cells); } } - st.rev += 1; + st.rev = st.rev.wrapping_add(1); } ok() }) diff --git a/crates/engine/src/cmd/text.rs b/crates/engine/src/cmd/text.rs index 45497a02d..2f8a3b345 100644 --- a/crates/engine/src/cmd/text.rs +++ b/crates/engine/src/cmd/text.rs @@ -801,7 +801,7 @@ pub(crate) fn set_story_direction(d: &mut designcraft_doc::Document, stories: &[ && st.vertical != vertical { st.vertical = vertical; - st.rev += 1; + st.rev = st.rev.wrapping_add(1); } } } @@ -1329,7 +1329,7 @@ pub(crate) fn apply_char_attrs(d: &mut designcraft_doc::Document, targets: &[Tar && let Some(run) = st.chars.first_mut() { run.format.over.merge(a); - st.rev += 1; + st.rev = st.rev.wrapping_add(1); } continue; } diff --git a/crates/engine/src/cmd/xml.rs b/crates/engine/src/cmd/xml.rs index 6dab205fa..45631b6cb 100644 --- a/crates/engine/src/cmd/xml.rs +++ b/crates/engine/src/cmd/xml.rs @@ -438,7 +438,7 @@ fn import_xml(s: &mut Session, p: &Value) -> Result { } } fresh.frames = frames; - fresh.rev = st.rev + 1; + fresh.rev = st.rev.wrapping_add(1); *st = fresh; } Ok(json!({"placed": placed, "unmatched": unmatched})) diff --git a/crates/engine/src/cmd/xref.rs b/crates/engine/src/cmd/xref.rs index b0b97ebd2..b5874fff0 100644 --- a/crates/engine/src/cmd/xref.rs +++ b/crates/engine/src/cmd/xref.rs @@ -162,7 +162,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result { let st = d.story_mut(sid).ok_or(designcraft_doc::DocError::NoStory(sid))?; let x = st.xrefs.get_mut(k).ok_or_else(|| bad("xref.setFormat", "no such cross-reference"))?; std::sync::Arc::make_mut(x).format = format; - st.rev += 1; + st.rev = st.rev.wrapping_add(1); Ok(Value::Null) }) }