Background save: dropped Puppet Warp pins, ended Edit Text and font scans leave the document saved

Selecting other art drops Puppet Warp pins, ending Edit Text clears the
flat view, and a font scan lays type out again. None of these makes an
undo step or counts as an edit to save, but each one replaced the
document. A background save that finished afterwards compared the
document with the one it wrote by pointer, so the document stayed
marked modified. Data Recovery also wrote a new copy of a document whose
pins had been dropped.

DocState now records the documents that these changes replaced, and
DocState::same_content counts them as having the same content. The
modified flag, the end of a background save and Data Recovery use it.
After Edit Text ends, an undo state that was the saved one stays the
saved one.
This commit is contained in:
dotCooCoo
2026-10-17 17:13:03 +07:00
committed by bflatastic
parent 1401051603
commit b0e78cf3d8
10 changed files with 181 additions and 26 deletions
+9 -12
View File
@@ -746,13 +746,9 @@ fn puppet_warp(s: &mut Session, p: &Value) -> Result<Value> {
}
/// Forget the Puppet Warp pins: not an undo step, and a saved document stays saved (pins are
/// never saved).
/// never saved, [`crate::DocState::change_unsaved`]).
pub(crate) fn drop_puppet_pins(st: &mut crate::DocState) {
let clean = !st.is_dirty();
Arc::make_mut(&mut st.doc).puppet = None;
if clean {
st.mark_saved();
}
st.change_unsaved(|d| Arc::make_mut(d).puppet = None);
}
fn puppet_pins(s: &mut Session, p: &Value) -> Result<Value> {
@@ -1335,7 +1331,8 @@ fn edit_text(s: &mut Session, p: &Value) -> Result<Value> {
}
/// Edit Text ends (isolation mode on `id` exits): the type is projected again, in the document and
/// in every undo state (the flat view is never an undo step of its own).
/// in every undo state (the flat view is never an undo step of its own, and never saved: a saved
/// state stays saved, [`crate::DocState::change_unsaved`]).
pub(crate) fn finish_edit_text(st: &mut crate::DocState, id: NodeId) {
let clear = |doc: &mut Arc<Document>| {
// Undo steps keep no lookup cache: look without building one, and drop the one an edit
@@ -1346,14 +1343,14 @@ pub(crate) fn finish_edit_text(st: &mut crate::DocState, id: NodeId) {
p.editing = false;
}
};
let saved = Arc::ptr_eq(&st.doc, &st.saved_doc);
clear(&mut st.doc);
if saved {
st.saved_doc = st.doc.clone();
}
st.change_unsaved(clear);
for e in st.history.undo.iter_mut().chain(st.history.redo.iter_mut()) {
let saved = Arc::ptr_eq(&e.doc, &st.saved_doc);
clear(&mut e.doc);
e.doc.drop_lookup_cache();
if saved {
st.saved_doc = e.doc.clone();
}
}
if let Some(it) = &mut st.interaction {
clear(&mut it.doc);
+8 -1
View File
@@ -648,7 +648,8 @@ impl SaveJob {
/// Once written: Save and Save As (except to a template) make the file `st`'s own: path,
/// title, format, options, and the snapshot as the saved state (edits made since the job was
/// made keep the document modified).
/// made keep the document modified; changes that are not edits to save don't,
/// [`DocState::same_content`]).
pub fn finish(self, st: &mut DocState) {
let retargets = self.plan.retargets();
if let (true, Some(path)) = (retargets, self.plan.path) {
@@ -660,6 +661,12 @@ impl SaveJob {
if let Some(at) = self.stamped {
if Arc::ptr_eq(&st.doc, &self.source) {
st.doc = self.snapshot.clone();
} else if st.same_content(&self.source) {
// Changed while the save ran only by changes that are not edits to save
// (Puppet Warp pins dropped, Edit Text ended, type laid out again after a font
// scan): with the dates, the document counts as saved.
stamp_save_dates(st, at);
return st.mark_saved();
} else {
// Edited while the save ran: the dates still go on, the edits stay unsaved. The
// undo history holds the document as it was before the dates, so that is the
+7 -9
View File
@@ -475,16 +475,14 @@ pub(crate) fn rescan(s: &mut Session, _: &Value) -> Result<Value> {
}
});
if !texts.is_empty() {
let saved = !d.is_dirty();
let doc = std::sync::Arc::make_mut(&mut d.doc);
for id in texts {
if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) {
refresh_bounds(t);
d.change_unsaved(|doc| {
let doc = std::sync::Arc::make_mut(doc);
for id in texts {
if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) {
refresh_bounds(t);
}
}
}
if saved {
d.mark_saved();
}
});
}
d.revision += 1;
}
+1 -1
View File
@@ -697,7 +697,7 @@ pub fn skip_reason(s: &Session, st: &DocState) -> Option<&'static str> {
if !st.is_dirty() {
return Some("no unsaved changes");
}
if st.recovery.as_ref().is_some_and(|c| Arc::ptr_eq(&c.doc, &st.doc)) {
if st.recovery.as_ref().is_some_and(|c| st.same_content(&c.doc)) {
return Some("unchanged since its last recovery copy");
}
if s.prefs.recovery_off_for_complex && object_count(&st.doc) > COMPLEX_OBJECTS {