Skip to main content

oxihuman_core/
undo_redo.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Generic undo/redo command history stack.
5
6#[allow(dead_code)]
7pub struct UndoCommand {
8    pub id: u64,
9    pub name: String,
10    pub data: Vec<u8>,
11    pub after: Vec<u8>,
12}
13
14#[allow(dead_code)]
15pub struct UndoStack {
16    pub history: Vec<UndoCommand>,
17    pub future: Vec<UndoCommand>,
18    pub max_depth: usize,
19    pub next_id: u64,
20}
21
22#[allow(dead_code)]
23pub fn new_undo_stack(max_depth: usize) -> UndoStack {
24    UndoStack {
25        history: Vec::new(),
26        future: Vec::new(),
27        max_depth,
28        next_id: 0,
29    }
30}
31
32#[allow(dead_code)]
33pub fn push_command(stack: &mut UndoStack, name: &str, before: Vec<u8>, after: Vec<u8>) {
34    stack.future.clear();
35    let id = stack.next_id;
36    stack.next_id += 1;
37    stack.history.push(UndoCommand {
38        id,
39        name: name.to_string(),
40        data: before,
41        after,
42    });
43    if stack.max_depth > 0 && stack.history.len() > stack.max_depth {
44        let overflow = stack.history.len() - stack.max_depth;
45        stack.history.drain(0..overflow);
46    }
47}
48
49#[allow(dead_code)]
50pub fn undo(stack: &mut UndoStack) -> Option<&UndoCommand> {
51    let cmd = stack.history.pop()?;
52    stack.future.push(cmd);
53    stack.future.last()
54}
55
56#[allow(dead_code)]
57pub fn redo(stack: &mut UndoStack) -> Option<&UndoCommand> {
58    let cmd = stack.future.pop()?;
59    stack.history.push(cmd);
60    stack.history.last()
61}
62
63#[allow(dead_code)]
64pub fn can_undo(stack: &UndoStack) -> bool {
65    !stack.history.is_empty()
66}
67
68#[allow(dead_code)]
69pub fn can_redo(stack: &UndoStack) -> bool {
70    !stack.future.is_empty()
71}
72
73#[allow(dead_code)]
74pub fn clear_undo_history(stack: &mut UndoStack) {
75    stack.history.clear();
76    stack.future.clear();
77}
78
79#[allow(dead_code)]
80pub fn history_depth(stack: &UndoStack) -> usize {
81    stack.history.len()
82}
83
84#[allow(dead_code)]
85pub fn future_depth(stack: &UndoStack) -> usize {
86    stack.future.len()
87}
88
89#[allow(dead_code)]
90pub fn peek_undo(stack: &UndoStack) -> Option<&UndoCommand> {
91    stack.history.last()
92}
93
94#[allow(dead_code)]
95pub fn peek_redo(stack: &UndoStack) -> Option<&UndoCommand> {
96    stack.future.last()
97}
98
99#[allow(dead_code)]
100pub fn command_names(stack: &UndoStack) -> Vec<&str> {
101    stack.history.iter().map(|c| c.name.as_str()).collect()
102}
103
104#[allow(dead_code)]
105pub fn truncate_history(stack: &mut UndoStack, keep: usize) {
106    if stack.history.len() > keep {
107        let drain_count = stack.history.len() - keep;
108        stack.history.drain(0..drain_count);
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_push_then_undo_returns_command() {
118        let mut stack = new_undo_stack(10);
119        push_command(&mut stack, "move", vec![1], vec![2]);
120        let cmd = undo(&mut stack);
121        assert!(cmd.is_some());
122        assert_eq!(cmd.expect("should succeed").name, "move");
123    }
124
125    #[test]
126    fn test_redo_after_undo() {
127        let mut stack = new_undo_stack(10);
128        push_command(&mut stack, "action", vec![0], vec![1]);
129        undo(&mut stack);
130        let redone = redo(&mut stack);
131        assert!(redone.is_some());
132        assert_eq!(redone.expect("should succeed").name, "action");
133    }
134
135    #[test]
136    fn test_can_undo_after_push() {
137        let mut stack = new_undo_stack(10);
138        assert!(!can_undo(&stack));
139        push_command(&mut stack, "a", vec![], vec![]);
140        assert!(can_undo(&stack));
141    }
142
143    #[test]
144    fn test_can_redo_after_undo() {
145        let mut stack = new_undo_stack(10);
146        push_command(&mut stack, "a", vec![], vec![]);
147        assert!(!can_redo(&stack));
148        undo(&mut stack);
149        assert!(can_redo(&stack));
150    }
151
152    #[test]
153    fn test_clear_history() {
154        let mut stack = new_undo_stack(10);
155        push_command(&mut stack, "a", vec![], vec![]);
156        push_command(&mut stack, "b", vec![], vec![]);
157        clear_undo_history(&mut stack);
158        assert!(!can_undo(&stack));
159        assert!(!can_redo(&stack));
160    }
161
162    #[test]
163    fn test_max_depth_enforced() {
164        let mut stack = new_undo_stack(3);
165        push_command(&mut stack, "a", vec![], vec![]);
166        push_command(&mut stack, "b", vec![], vec![]);
167        push_command(&mut stack, "c", vec![], vec![]);
168        push_command(&mut stack, "d", vec![], vec![]);
169        assert_eq!(history_depth(&stack), 3);
170    }
171
172    #[test]
173    fn test_redo_cleared_on_new_push() {
174        let mut stack = new_undo_stack(10);
175        push_command(&mut stack, "a", vec![], vec![]);
176        undo(&mut stack);
177        assert!(can_redo(&stack));
178        push_command(&mut stack, "b", vec![], vec![]);
179        assert!(!can_redo(&stack));
180    }
181
182    #[test]
183    fn test_peek_undo_does_not_pop() {
184        let mut stack = new_undo_stack(10);
185        push_command(&mut stack, "peek_test", vec![], vec![]);
186        let _ = peek_undo(&stack);
187        assert!(can_undo(&stack));
188        assert_eq!(history_depth(&stack), 1);
189    }
190
191    #[test]
192    fn test_peek_redo_does_not_pop() {
193        let mut stack = new_undo_stack(10);
194        push_command(&mut stack, "a", vec![], vec![]);
195        undo(&mut stack);
196        let _ = peek_redo(&stack);
197        assert!(can_redo(&stack));
198        assert_eq!(future_depth(&stack), 1);
199    }
200
201    #[test]
202    fn test_history_depth() {
203        let mut stack = new_undo_stack(10);
204        assert_eq!(history_depth(&stack), 0);
205        push_command(&mut stack, "a", vec![], vec![]);
206        assert_eq!(history_depth(&stack), 1);
207    }
208
209    #[test]
210    fn test_future_depth() {
211        let mut stack = new_undo_stack(10);
212        push_command(&mut stack, "a", vec![], vec![]);
213        push_command(&mut stack, "b", vec![], vec![]);
214        undo(&mut stack);
215        assert_eq!(future_depth(&stack), 1);
216    }
217
218    #[test]
219    fn test_command_names() {
220        let mut stack = new_undo_stack(10);
221        push_command(&mut stack, "first", vec![], vec![]);
222        push_command(&mut stack, "second", vec![], vec![]);
223        let names = command_names(&stack);
224        assert_eq!(names, vec!["first", "second"]);
225    }
226
227    #[test]
228    fn test_truncate_history() {
229        let mut stack = new_undo_stack(10);
230        push_command(&mut stack, "a", vec![], vec![]);
231        push_command(&mut stack, "b", vec![], vec![]);
232        push_command(&mut stack, "c", vec![], vec![]);
233        truncate_history(&mut stack, 2);
234        assert_eq!(history_depth(&stack), 2);
235    }
236}