Engine guard: a recovered panic restores every document's undo history and the active document (#3599)

run_guarded put back only the active document's art, selection and interaction.
An undo step pushed before the panic stayed (Undo then stepped to a state equal
to the current one) and Redo was lost; other documents a command touched kept
their half-applied edits; the active document was restored by index.

It now snapshots every open document with its history and restores them, and
the active document by uid.

Fixes #3599
This commit is contained in:
marshiyar
2026-10-31 19:43:17 +07:00
committed by bflatastic
parent 7403003fb1
commit 4861fb9d85
2 changed files with 52 additions and 9 deletions
+12 -9
View File
@@ -199,27 +199,30 @@ impl Session {
r
}
/// A top-level command: a panic (a bug) becomes [`EngineError::Internal`] and the active
/// document goes back to how it was before the command, instead of crashing the frontend.
/// A top-level command: a panic (a bug) becomes [`EngineError::Internal`] and every open
/// document goes back to how it was before the command (its art, selection, interaction and
/// undo history), and the same document stays active, instead of crashing the frontend.
pub(crate) fn run_guarded(&mut self, id: &str, f: impl FnOnce(&mut Self) -> Result<Value>) -> Result<Value> {
let snapshot = self.active().map(|d| (d.uid, d.doc.clone(), d.selection.clone(), d.interaction.clone()));
let active = self.active;
let snapshot: Vec<_> =
self.docs.iter().map(|d| (d.uid, d.doc.clone(), d.selection.clone(), d.interaction.clone(), d.history.clone())).collect();
let active = self.active().map(|d| d.uid);
let r = match guard::catch_panic(|| self.run_nested(f)) {
Ok(r) => r,
Err(msg) => {
self.depth = 0;
self.journal_note.clear();
if let Some((uid, doc, selection, interaction)) = snapshot
&& let Some(st) = self.docs.iter_mut().find(|d| d.uid == uid)
{
for (uid, doc, selection, interaction, history) in snapshot {
// A document the command closed stays closed.
let Some(st) = self.docs.iter_mut().find(|d| d.uid == uid) else { continue };
st.doc = doc;
st.selection = selection;
st.interaction = interaction;
st.history = history;
cmd::globaledit::follow_history(st);
st.revision += 1;
}
if active.is_some_and(|i| i < self.docs.len()) {
self.active = active;
if let Some(i) = active.and_then(|uid| self.docs.iter().position(|d| d.uid == uid)) {
self.active = Some(i);
}
Err(EngineError::Internal { cmd: id.to_string(), msg })
}