Skip to main content

mockforge_foundation/state_machine/
history.rs

1//! Undo/redo history manager for state machine edits
2//!
3//! Provides history tracking for state machine modifications, enabling undo/redo
4//! functionality in visual editors.
5
6use crate::state_machine::rules::StateMachine;
7use std::collections::VecDeque;
8
9/// History manager for state machine edits
10///
11/// Maintains undo and redo stacks for state machine modifications.
12/// Supports configurable maximum history size to limit memory usage.
13#[derive(Debug, Clone)]
14pub struct HistoryManager {
15    /// Stack of previous states (for undo)
16    undo_stack: VecDeque<StateMachine>,
17
18    /// Stack of future states (for redo)
19    redo_stack: VecDeque<StateMachine>,
20
21    /// Maximum number of history entries to keep
22    max_history: usize,
23
24    /// Current state (not yet committed to history)
25    current: Option<StateMachine>,
26}
27
28impl HistoryManager {
29    /// Create a new history manager with default max history (50)
30    pub fn new() -> Self {
31        Self::with_max_history(50)
32    }
33
34    /// Create a new history manager with specified max history
35    pub fn with_max_history(max_history: usize) -> Self {
36        Self {
37            undo_stack: VecDeque::new(),
38            redo_stack: VecDeque::new(),
39            max_history,
40            current: None,
41        }
42    }
43
44    /// Push a new state to the history
45    ///
46    /// If there's a current state, it's moved to the undo stack.
47    /// The new state becomes the current state.
48    pub fn push_state(&mut self, state: StateMachine) {
49        // If we have a current state, move it to undo stack
50        if let Some(current) = self.current.take() {
51            // Limit undo stack size
52            if self.undo_stack.len() >= self.max_history {
53                self.undo_stack.pop_front();
54            }
55            self.undo_stack.push_back(current);
56        }
57
58        // Clear redo stack when new state is pushed
59        self.redo_stack.clear();
60
61        // Set new current state
62        self.current = Some(state);
63    }
64
65    /// Undo the last change
66    ///
67    /// Moves the current state to the redo stack and restores the previous
68    /// state from the undo stack. Returns the restored state if available.
69    pub fn undo(&mut self) -> Option<StateMachine> {
70        if self.undo_stack.is_empty() {
71            return None;
72        }
73
74        // Move current to redo stack
75        if let Some(current) = self.current.take() {
76            // Limit redo stack size
77            if self.redo_stack.len() >= self.max_history {
78                self.redo_stack.pop_front();
79            }
80            self.redo_stack.push_back(current);
81        }
82
83        // Restore from undo stack
84        let restored = self.undo_stack.pop_back()?;
85        self.current = Some(restored.clone());
86        Some(restored)
87    }
88
89    /// Redo the last undone change
90    ///
91    /// Moves the current state to the undo stack and restores the next
92    /// state from the redo stack. Returns the restored state if available.
93    pub fn redo(&mut self) -> Option<StateMachine> {
94        if self.redo_stack.is_empty() {
95            return None;
96        }
97
98        // Move current to undo stack
99        if let Some(current) = self.current.take() {
100            // Limit undo stack size
101            if self.undo_stack.len() >= self.max_history {
102                self.undo_stack.pop_front();
103            }
104            self.undo_stack.push_back(current);
105        }
106
107        // Restore from redo stack
108        let restored = self.redo_stack.pop_back()?;
109        self.current = Some(restored.clone());
110        Some(restored)
111    }
112
113    /// Check if undo is possible
114    pub fn can_undo(&self) -> bool {
115        !self.undo_stack.is_empty()
116    }
117
118    /// Check if redo is possible
119    pub fn can_redo(&self) -> bool {
120        !self.redo_stack.is_empty()
121    }
122
123    /// Get the current state
124    pub fn current(&self) -> Option<&StateMachine> {
125        self.current.as_ref()
126    }
127
128    /// Get the current state mutably
129    pub fn current_mut(&mut self) -> Option<&mut StateMachine> {
130        self.current.as_mut()
131    }
132
133    /// Set the current state without adding to history
134    ///
135    /// Useful for temporary edits that shouldn't be tracked.
136    pub fn set_current(&mut self, state: StateMachine) {
137        self.current = Some(state);
138    }
139
140    /// Clear all history
141    pub fn clear(&mut self) {
142        self.undo_stack.clear();
143        self.redo_stack.clear();
144        self.current = None;
145    }
146
147    /// Get the number of undo steps available
148    pub fn undo_count(&self) -> usize {
149        self.undo_stack.len()
150    }
151
152    /// Get the number of redo steps available
153    pub fn redo_count(&self) -> usize {
154        self.redo_stack.len()
155    }
156
157    /// Get the maximum history size
158    pub fn max_history(&self) -> usize {
159        self.max_history
160    }
161
162    /// Set the maximum history size
163    ///
164    /// If the new max is smaller than current history, older entries are removed.
165    pub fn set_max_history(&mut self, max_history: usize) {
166        self.max_history = max_history;
167
168        // Trim undo stack if needed
169        while self.undo_stack.len() > max_history {
170            self.undo_stack.pop_front();
171        }
172
173        // Trim redo stack if needed
174        while self.redo_stack.len() > max_history {
175            self.redo_stack.pop_front();
176        }
177    }
178}
179
180impl Default for HistoryManager {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189    use crate::state_machine::rules::{StateMachine, StateTransition};
190
191    fn create_test_machine(name: &str) -> StateMachine {
192        StateMachine::new(
193            format!("resource_{}", name),
194            vec!["state1".to_string(), "state2".to_string()],
195            "state1",
196        )
197        .add_transition(StateTransition::new("state1", "state2"))
198    }
199
200    #[test]
201    fn test_history_manager_creation() {
202        let manager = HistoryManager::new();
203        assert!(!manager.can_undo());
204        assert!(!manager.can_redo());
205    }
206
207    #[test]
208    fn test_push_and_undo() {
209        let mut manager = HistoryManager::new();
210        let state1 = create_test_machine("1");
211        let state2 = create_test_machine("2");
212
213        manager.push_state(state1.clone());
214        manager.push_state(state2.clone());
215
216        assert!(manager.can_undo());
217        let restored = manager.undo().unwrap();
218        assert_eq!(restored.resource_type, state1.resource_type);
219    }
220
221    #[test]
222    fn test_undo_redo() {
223        let mut manager = HistoryManager::new();
224        let state1 = create_test_machine("1");
225        let state2 = create_test_machine("2");
226
227        manager.push_state(state1.clone());
228        manager.push_state(state2.clone());
229
230        // Undo
231        let restored = manager.undo().unwrap();
232        assert_eq!(restored.resource_type, state1.resource_type);
233
234        // Redo
235        let restored = manager.redo().unwrap();
236        assert_eq!(restored.resource_type, state2.resource_type);
237    }
238
239    #[test]
240    fn test_max_history() {
241        let mut manager = HistoryManager::with_max_history(3);
242
243        for i in 0..5 {
244            manager.push_state(create_test_machine(&i.to_string()));
245        }
246
247        // Should only keep last 3 in undo stack
248        assert_eq!(manager.undo_count(), 3);
249    }
250
251    #[test]
252    fn test_clear_redo_on_new_push() {
253        let mut manager = HistoryManager::new();
254        let state1 = create_test_machine("1");
255        let state2 = create_test_machine("2");
256        let state3 = create_test_machine("3");
257
258        manager.push_state(state1);
259        manager.push_state(state2);
260        manager.undo(); // Now we can redo
261        assert!(manager.can_redo());
262
263        manager.push_state(state3); // This should clear redo
264        assert!(!manager.can_redo());
265    }
266}