mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-25 18:56:50 -06:00
Merge #456: Wrap saved story revision counters consistently
Conflict: crates/doc/src/edit.rs: next's thread_frames (append_story); every rev += 1 is rev.wrapping_add(1) Conflict: crates/engine/src/cmd/fonts.rs: next's replace (the local-formatting pass moved out of this hunk) Conflict: crates/engine/src/cmd/text.rs: next's format_chars (apply_char_attrs, typing format); rev += 1 is rev.wrapping_add(1)
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ impl Story {
|
||||
}
|
||||
self.fix_tables();
|
||||
}
|
||||
self.rev += 1;
|
||||
self.rev = self.rev.wrapping_add(1);
|
||||
end
|
||||
}
|
||||
|
||||
|
||||
+29
-4
@@ -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");
|
||||
|
||||
@@ -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() };
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
if let Some(a) = author {
|
||||
n.author = a;
|
||||
}
|
||||
st.rev += 1;
|
||||
st.rev = st.rev.wrapping_add(1);
|
||||
ok()
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Value> {
|
||||
ts.apply_to(std::sync::Arc::make_mut(t), &cells);
|
||||
}
|
||||
}
|
||||
st.rev += 1;
|
||||
st.rev = st.rev.wrapping_add(1);
|
||||
}
|
||||
ok()
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ fn import_xml(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
fresh.frames = frames;
|
||||
fresh.rev = st.rev + 1;
|
||||
fresh.rev = st.rev.wrapping_add(1);
|
||||
*st = fresh;
|
||||
}
|
||||
Ok(json!({"placed": placed, "unmatched": unmatched}))
|
||||
|
||||
@@ -162,7 +162,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user