From 2076f82b47ae6e4bfb012446fcb1aa73c1310bca Mon Sep 17 00:00:00 2001 From: theironchef <6278036+theironchef@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:39:52 -0700 Subject: [PATCH] Edit: Undo during typing or a drag takes back only that, and Redo returns it edit.undo cancelled an open interaction (rolling back the typing session or drag preview without recording it) and then undid the step before it: one Undo removed two steps, and the typing could not be redone. Undo now commits the interaction first, so it is the step taken back and Redo can restore it; commit_interaction does nothing when nothing is open or nothing changed. Fixes #159 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D --- crates/engine/src/cmd/edit.rs | 4 ++- crates/engine/src/tests_textedit.rs | 38 +++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index fa58a619..363d56a9 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -117,7 +117,9 @@ fn set_units(s: &mut Session, p: &Value) -> Result { } fn undo(s: &mut Session, _: &Value) -> Result { - s.cancel_interaction()?; + // What is still in progress (typing, a drag) is the step to undo: keep it first, so Undo takes + // back only that and Redo brings it back, instead of dropping it and undoing the step before. + s.commit_interaction()?; let st = s.doc_mut()?; let e = st.history.undo.pop().ok_or_else(|| EngineError::Other("nothing to undo".into()))?; let label = e.label.clone(); diff --git a/crates/engine/src/tests_textedit.rs b/crates/engine/src/tests_textedit.rs index 07b908c2..d89d4645 100644 --- a/crates/engine/src/tests_textedit.rs +++ b/crates/engine/src/tests_textedit.rs @@ -119,6 +119,44 @@ fn type_tool_typing_is_one_undo_step() { assert_eq!(obj(&s, id).plain_text(), ""); } +#[test] +fn undo_while_typing_takes_back_only_the_typing_and_redo_returns_it() { + let mut s = session(); + let v = ViewInfo::default(); + s.select_tool("type", v).unwrap(); + click(&mut s, 200.0, 200.0); + let id = s.doc().unwrap().selection.objects[0]; + s.tool_text("abc", v).unwrap(); + key(&mut s, ToolKey::Left, Mods::default()); + key(&mut s, ToolKey::Right, Mods::default()); + s.tool_text("def", v).unwrap(); + assert!(s.in_interaction(), "still typing"); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(obj(&s, id).plain_text(), "abc", "only the typing in progress is undone"); + s.execute("edit.redo", &json!({})).unwrap(); + assert_eq!(obj(&s, id).plain_text(), "abcdef", "and Redo brings it back"); + // Typing on after an undo starts a new session from the restored text. + s.execute("edit.undo", &json!({})).unwrap(); + s.tool_text("!", v).unwrap(); + assert_eq!(obj(&s, id).plain_text(), "abc!"); +} + +#[test] +fn undo_during_a_drag_takes_back_only_the_drag() { + let mut s = session(); + let v = ViewInfo::default(); + let r = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 50, "height": 50})).unwrap(); + let id = NodeId(r["id"].as_u64().unwrap()); + s.select_tool("selection", v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Down, 125.0, 125.0), v).unwrap(); + s.pointer(&PointerEvent::new(PointerKind::Drag, 225.0, 125.0), v).unwrap(); + assert!(s.in_interaction(), "dragging"); + s.execute("edit.undo", &json!({})).unwrap(); + let doc = &s.doc().unwrap().doc; + let b = doc.bounds_of(&[id], false).expect("the rectangle is still there"); + assert_eq!(b.x0, 100.0, "back where it was: {b:?}"); +} + #[test] fn type_tool_selection_replace_and_delete() { let mut s = session();