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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9TrG4w49oSRYbP3cqxA3D
This commit is contained in:
theironchef 2026-10-05 10:39:52 -07:00
parent 679191b4ca
commit 2076f82b47
2 changed files with 41 additions and 1 deletions

View File

@ -117,7 +117,9 @@ fn set_units(s: &mut Session, p: &Value) -> Result<Value> {
}
fn undo(s: &mut Session, _: &Value) -> Result<Value> {
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();

View File

@ -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();