feat(engine): add history.undo and history.redo aliases (#3675) (#3786)

A `history.*` namespace exists (history.deleteState, history.newDocument) but not history.undo, so a client or agent guessing `history.undo` gets a hard `unknown command`. Register history.undo and history.redo as aliases of edit.undo and edit.redo, with no menu entry and no shortcut; the Edit menu items keep Cmd+Z / Cmd+Shift+Z.

Tests: cargo test -p photocraft-engine --lib history_cmds; cargo test -p photocraft-engine --test panic_hunt -- --ignored; cargo xtask parity.
This commit is contained in:
Scott C Hughes authored and GitHub committed 2026-10-12 07:49:26 -04:00
1 parent c5da1a0d5f
commit 2aa1f8d2c6
1 file changed
+41
+41
View File
@@ -199,8 +199,39 @@ fn has_doc(s: &Session) -> std::result::Result<(), String> {
s.active().map(|_| ()).ok_or_else(|| "no document open".into())
}
/// `history.undo` / `history.redo` enable predicates, matching `edit.undo` / `edit.redo`.
fn can_undo(s: &Session) -> std::result::Result<(), String> {
s.active().filter(|d| d.history.can_undo()).map(|_| ()).ok_or_else(|| "nothing to undo".into())
}
fn can_redo(s: &Session) -> std::result::Result<(), String> {
s.active().filter(|d| d.history.can_redo()).map(|_| ()).ok_or_else(|| "nothing to redo".into())
}
pub fn specs() -> Vec<CommandSpec> {
vec![
// `history.*` aliases of Edit › Undo / Redo, so a client reaching for the History namespace
// finds them (`edit.undo` / `edit.redo` remain the menu items and own the shortcuts, #3675).
CommandSpec {
id: "history.undo",
label: "Undo",
menu: &[],
shortcut: None,
params: "{} (the same action as `edit.undo`)",
enabled: can_undo,
run: |s, _| Ok(json!(s.undo())),
journal: false,
},
CommandSpec {
id: "history.redo",
label: "Redo",
menu: &[],
shortcut: None,
params: "{} (the same action as `edit.redo`)",
enabled: can_redo,
run: |s, _| Ok(json!(s.redo())),
journal: false,
},
CommandSpec {
id: "history.deleteState",
label: "Delete State",
@@ -282,6 +313,16 @@ mod tests {
s.active().unwrap().history.entries()
}
#[test]
fn history_undo_and_redo_alias_edit_undo_and_redo() {
let mut s = session();
let n = s.active().unwrap().history.past_len();
s.execute("history.undo", json!({})).unwrap();
assert_eq!(s.active().unwrap().history.past_len(), n - 1, "history.undo steps back like edit.undo");
s.execute("history.redo", json!({})).unwrap();
assert_eq!(s.active().unwrap().history.past_len(), n, "history.redo steps forward like edit.redo");
}
#[test]
fn delete_current_state_steps_back_and_drops_it() {
let mut s = session();