ricecoder_undo_redo/
history.rs

1//! History management and navigation
2
3use crate::change::Change;
4use crate::error::UndoRedoError;
5use serde::{Deserialize, Serialize};
6
7/// Represents a single entry in the change history
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct HistoryEntry {
10    /// The change associated with this entry
11    pub change: Change,
12    /// Position in the history
13    pub index: usize,
14    /// Whether this change is currently undone
15    pub is_undone: bool,
16}
17
18impl HistoryEntry {
19    /// Create a new history entry
20    pub fn new(change: Change, index: usize) -> Self {
21        HistoryEntry {
22            change,
23            index,
24            is_undone: false,
25        }
26    }
27}
28
29/// Manages undo/redo stacks and change history
30pub struct HistoryManager {
31    undo_stack: Vec<Change>,
32    redo_stack: Vec<Change>,
33    all_changes: Vec<HistoryEntry>,
34}
35
36impl HistoryManager {
37    /// Create a new history manager
38    pub fn new() -> Self {
39        HistoryManager {
40            undo_stack: Vec::new(),
41            redo_stack: Vec::new(),
42            all_changes: Vec::new(),
43        }
44    }
45
46    /// Record a change in history
47    pub fn record_change(&mut self, change: Change) -> Result<(), UndoRedoError> {
48        change.validate()?;
49
50        // Add to undo stack
51        self.undo_stack.push(change.clone());
52
53        // Clear redo stack when new change is recorded
54        self.redo_stack.clear();
55
56        // Add to all_changes history
57        let index = self.all_changes.len();
58        self.all_changes.push(HistoryEntry::new(change, index));
59
60        Ok(())
61    }
62
63    /// Perform an undo operation
64    pub fn undo(&mut self) -> Result<Change, UndoRedoError> {
65        let change = self
66            .undo_stack
67            .pop()
68            .ok_or(UndoRedoError::NoMoreUndos)?;
69
70        // Mark as undone in history
71        if let Some(entry) = self.all_changes.iter_mut().rev().find(|e| e.change.id == change.id)
72        {
73            entry.is_undone = true;
74        }
75
76        // Add to redo stack
77        self.redo_stack.push(change.clone());
78
79        Ok(change)
80    }
81
82    /// Perform a redo operation
83    pub fn redo(&mut self) -> Result<Change, UndoRedoError> {
84        let change = self
85            .redo_stack
86            .pop()
87            .ok_or(UndoRedoError::NoMoreRedos)?;
88
89        // Mark as not undone in history
90        if let Some(entry) = self.all_changes.iter_mut().rev().find(|e| e.change.id == change.id)
91        {
92            entry.is_undone = false;
93        }
94
95        // Add back to undo stack
96        self.undo_stack.push(change.clone());
97
98        Ok(change)
99    }
100
101    /// Check if undo is available
102    pub fn can_undo(&self) -> bool {
103        !self.undo_stack.is_empty()
104    }
105
106    /// Check if redo is available
107    pub fn can_redo(&self) -> bool {
108        !self.redo_stack.is_empty()
109    }
110
111    /// Get paginated history
112    pub fn get_history(&self, limit: usize, offset: usize) -> Vec<HistoryEntry> {
113        self.all_changes
114            .iter()
115            .skip(offset)
116            .take(limit)
117            .cloned()
118            .collect()
119    }
120
121    /// Get details of a specific change
122    pub fn get_change_details(&self, change_id: &str) -> Result<HistoryEntry, UndoRedoError> {
123        self.all_changes
124            .iter()
125            .find(|e| e.change.id == change_id)
126            .cloned()
127            .ok_or_else(|| UndoRedoError::change_not_found(change_id))
128    }
129
130    /// Get all changes for a specific file
131    pub fn get_changes_by_file(&self, file_path: &str) -> Vec<HistoryEntry> {
132        self.all_changes
133            .iter()
134            .filter(|e| e.change.file_path == file_path)
135            .cloned()
136            .collect()
137    }
138
139    /// Get the total number of changes in history
140    pub fn total_changes(&self) -> usize {
141        self.all_changes.len()
142    }
143
144    /// Get the number of undoable changes
145    pub fn undoable_count(&self) -> usize {
146        self.undo_stack.len()
147    }
148
149    /// Get the number of redoable changes
150    pub fn redoable_count(&self) -> usize {
151        self.redo_stack.len()
152    }
153}
154
155impl Default for HistoryManager {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164    use crate::change::ChangeType;
165
166    #[test]
167    fn test_history_manager_record_change() {
168        let mut manager = HistoryManager::new();
169        let change = Change::new(
170            "test.txt",
171            "before",
172            "after",
173            "Modify",
174            ChangeType::Modify,
175        )
176        .unwrap();
177        let result = manager.record_change(change);
178        assert!(result.is_ok());
179        assert_eq!(manager.total_changes(), 1);
180        assert!(manager.can_undo());
181    }
182
183    #[test]
184    fn test_history_manager_undo() {
185        let mut manager = HistoryManager::new();
186        let change = Change::new(
187            "test.txt",
188            "before",
189            "after",
190            "Modify",
191            ChangeType::Modify,
192        )
193        .unwrap();
194        manager.record_change(change.clone()).unwrap();
195
196        let undone = manager.undo();
197        assert!(undone.is_ok());
198        assert!(!manager.can_undo());
199        assert!(manager.can_redo());
200    }
201
202    #[test]
203    fn test_history_manager_redo() {
204        let mut manager = HistoryManager::new();
205        let change = Change::new(
206            "test.txt",
207            "before",
208            "after",
209            "Modify",
210            ChangeType::Modify,
211        )
212        .unwrap();
213        manager.record_change(change).unwrap();
214        manager.undo().unwrap();
215
216        let redone = manager.redo();
217        assert!(redone.is_ok());
218        assert!(manager.can_undo());
219        assert!(!manager.can_redo());
220    }
221
222    #[test]
223    fn test_history_manager_no_more_undos() {
224        let mut manager = HistoryManager::new();
225        let result = manager.undo();
226        assert!(result.is_err());
227        assert!(matches!(result, Err(UndoRedoError::NoMoreUndos)));
228    }
229
230    #[test]
231    fn test_history_manager_no_more_redos() {
232        let mut manager = HistoryManager::new();
233        let result = manager.redo();
234        assert!(result.is_err());
235        assert!(matches!(result, Err(UndoRedoError::NoMoreRedos)));
236    }
237
238    #[test]
239    fn test_history_manager_redo_stack_clearing() {
240        let mut manager = HistoryManager::new();
241        let change1 = Change::new(
242            "test.txt",
243            "before1",
244            "after1",
245            "Modify 1",
246            ChangeType::Modify,
247        )
248        .unwrap();
249        let change2 = Change::new(
250            "test.txt",
251            "after1",
252            "after2",
253            "Modify 2",
254            ChangeType::Modify,
255        )
256        .unwrap();
257
258        manager.record_change(change1).unwrap();
259        manager.undo().unwrap();
260        assert!(manager.can_redo());
261
262        // Record new change after undo
263        manager.record_change(change2).unwrap();
264        assert!(!manager.can_redo());
265    }
266
267    #[test]
268    fn test_history_manager_get_history() {
269        let mut manager = HistoryManager::new();
270        for i in 0..5 {
271            let change = Change::new(
272                "test.txt",
273                &format!("before{}", i),
274                &format!("after{}", i),
275                &format!("Modify {}", i),
276                ChangeType::Modify,
277            )
278            .unwrap();
279            manager.record_change(change).unwrap();
280        }
281
282        let history = manager.get_history(2, 1);
283        assert_eq!(history.len(), 2);
284        assert_eq!(history[0].index, 1);
285    }
286
287    #[test]
288    fn test_history_manager_get_change_details() {
289        let mut manager = HistoryManager::new();
290        let change = Change::new(
291            "test.txt",
292            "before",
293            "after",
294            "Modify",
295            ChangeType::Modify,
296        )
297        .unwrap();
298        let change_id = change.id.clone();
299        manager.record_change(change).unwrap();
300
301        let details = manager.get_change_details(&change_id);
302        assert!(details.is_ok());
303        assert_eq!(details.unwrap().change.id, change_id);
304    }
305
306    #[test]
307    fn test_history_manager_get_changes_by_file() {
308        let mut manager = HistoryManager::new();
309        let change1 = Change::new(
310            "file1.txt",
311            "before",
312            "after",
313            "Modify",
314            ChangeType::Modify,
315        )
316        .unwrap();
317        let change2 = Change::new(
318            "file2.txt",
319            "before",
320            "after",
321            "Modify",
322            ChangeType::Modify,
323        )
324        .unwrap();
325        let change3 = Change::new(
326            "file1.txt",
327            "before",
328            "after",
329            "Modify",
330            ChangeType::Modify,
331        )
332        .unwrap();
333
334        manager.record_change(change1).unwrap();
335        manager.record_change(change2).unwrap();
336        manager.record_change(change3).unwrap();
337
338        let file1_changes = manager.get_changes_by_file("file1.txt");
339        assert_eq!(file1_changes.len(), 2);
340    }
341}
342
343#[cfg(test)]
344mod property_tests {
345    use super::*;
346    use crate::change::ChangeType;
347    use proptest::prelude::*;
348
349    /// Strategy for generating valid file paths
350    fn file_path_strategy() -> impl Strategy<Value = String> {
351        r"[a-zA-Z0-9_\-./]{1,50}\.rs"
352            .prop_map(|s| s.to_string())
353    }
354
355    /// Strategy for generating valid content (non-empty, non-whitespace-only)
356    fn content_strategy() -> impl Strategy<Value = String> {
357        r"[a-zA-Z0-9]{1,100}"
358            .prop_map(|s| s.to_string())
359    }
360
361    proptest! {
362        /// **Feature: ricecoder-undo-redo, Property 1: Undo/Redo Consistency**
363        /// *For any* sequence of changes, performing undo followed by redo SHALL restore
364        /// the original state exactly.
365        /// **Validates: Requirements 2.1, 2.2**
366        #[test]
367        fn prop_undo_redo_consistency(
368            changes_data in prop::collection::vec(
369                (file_path_strategy(), content_strategy(), content_strategy()),
370                1..10
371            ),
372        ) {
373            let mut manager = HistoryManager::new();
374            let mut recorded_changes = Vec::new();
375
376            // Record a sequence of changes
377            for (idx, (file_path, before, after)) in changes_data.iter().enumerate() {
378                // Ensure before and after are different for modify operations
379                prop_assume!(before != after);
380
381                let change = Change::new(
382                    file_path.clone(),
383                    before.clone(),
384                    after.clone(),
385                    format!("Change {}", idx),
386                    ChangeType::Modify,
387                )
388                .unwrap();
389
390                recorded_changes.push(change.clone());
391                manager.record_change(change).ok();
392            }
393
394            // Verify we recorded all changes
395            prop_assert_eq!(
396                manager.total_changes(),
397                recorded_changes.len(),
398                "All changes should be recorded"
399            );
400
401            // Perform undo operations for all changes
402            let mut undone_changes = Vec::new();
403            while manager.can_undo() {
404                if let Ok(change) = manager.undo() {
405                    undone_changes.push(change);
406                }
407            }
408
409            // Verify all changes were undone
410            prop_assert_eq!(
411                undone_changes.len(),
412                recorded_changes.len(),
413                "All changes should be undoable"
414            );
415
416            // Perform redo operations for all undone changes
417            let mut redone_changes = Vec::new();
418            while manager.can_redo() {
419                if let Ok(change) = manager.redo() {
420                    redone_changes.push(change);
421                }
422            }
423
424            // Verify all changes were redone
425            prop_assert_eq!(
426                redone_changes.len(),
427                recorded_changes.len(),
428                "All changes should be redoable"
429            );
430
431            // Verify state is restored: undone changes in reverse order should match redone changes
432            for (undone, redone) in undone_changes.iter().zip(redone_changes.iter().rev()) {
433                prop_assert_eq!(
434                    &undone.id, &redone.id,
435                    "Undone and redone changes should have same ID"
436                );
437                prop_assert_eq!(
438                    &undone.file_path, &redone.file_path,
439                    "File paths should match"
440                );
441                prop_assert_eq!(
442                    &undone.before, &redone.before,
443                    "Before states should match"
444                );
445                prop_assert_eq!(
446                    &undone.after, &redone.after,
447                    "After states should match"
448                );
449            }
450
451            // Verify state after all redo operations
452            // After redoing all changes, we should be able to undo them again
453            prop_assert!(manager.can_undo(), "Should be able to undo after redo");
454            prop_assert!(!manager.can_redo(), "No more redos should be available after redo");
455        }
456
457        /// **Feature: ricecoder-undo-redo, Property 4: Redo Stack Clearing**
458        /// *For any* new change recorded after an undo operation, the redo stack SHALL be
459        /// cleared and no previously undone changes SHALL be reapplicable.
460        /// **Validates: Requirements 2.5**
461        #[test]
462        fn prop_redo_stack_clearing(
463            initial_changes in prop::collection::vec(
464                (file_path_strategy(), content_strategy(), content_strategy()),
465                1..5
466            ),
467            new_change_data in (file_path_strategy(), content_strategy(), content_strategy()),
468        ) {
469            let mut manager = HistoryManager::new();
470
471            // Record initial changes
472            for (idx, (file_path, before, after)) in initial_changes.iter().enumerate() {
473                prop_assume!(before != after);
474
475                let change = Change::new(
476                    file_path.clone(),
477                    before.clone(),
478                    after.clone(),
479                    format!("Initial {}", idx),
480                    ChangeType::Modify,
481                )
482                .unwrap();
483
484                manager.record_change(change).ok();
485            }
486
487            let initial_count = manager.total_changes();
488            prop_assume!(initial_count > 0);
489
490            // Perform undo
491            let undo_result = manager.undo();
492            prop_assert!(undo_result.is_ok(), "Undo should succeed");
493            prop_assert!(manager.can_redo(), "Redo should be available after undo");
494
495            // Record a new change after undo
496            let (file_path, before, after) = new_change_data;
497            prop_assume!(before != after);
498
499            let new_change = Change::new(
500                file_path,
501                before,
502                after,
503                "New change after undo",
504                ChangeType::Modify,
505            )
506            .unwrap();
507
508            manager.record_change(new_change).ok();
509
510            // Verify redo stack is cleared
511            prop_assert!(
512                !manager.can_redo(),
513                "Redo stack should be cleared after new change"
514            );
515
516            // Verify we can still undo the new change
517            prop_assert!(
518                manager.can_undo(),
519                "Should be able to undo the new change"
520            );
521
522            // Verify total changes increased by 1
523            prop_assert_eq!(
524                manager.total_changes(),
525                initial_count + 1,
526                "Total changes should increase by 1"
527            );
528        }
529
530        /// **Feature: ricecoder-undo-redo, Property 1: Undo/Redo Consistency**
531        /// *For any* single change, undo followed by redo should restore exact state.
532        /// **Validates: Requirements 2.1, 2.2**
533        #[test]
534        fn prop_single_change_undo_redo(
535            file_path in file_path_strategy(),
536            before in content_strategy(),
537            after in content_strategy(),
538        ) {
539            prop_assume!(before != after);
540
541            let mut manager = HistoryManager::new();
542            let change = Change::new(
543                &file_path,
544                &before,
545                &after,
546                "Test change",
547                ChangeType::Modify,
548            )
549            .unwrap();
550
551            let original_id = change.id.clone();
552            manager.record_change(change).unwrap();
553
554            // Verify can undo
555            prop_assert!(manager.can_undo(), "Should be able to undo");
556            prop_assert!(!manager.can_redo(), "Should not be able to redo initially");
557
558            // Perform undo
559            let undone = manager.undo().unwrap();
560            prop_assert_eq!(&undone.id, &original_id, "Undone change should have same ID");
561            prop_assert_eq!(&undone.file_path, &file_path, "File path should match");
562            prop_assert_eq!(&undone.before, &before, "Before state should match");
563            prop_assert_eq!(&undone.after, &after, "After state should match");
564
565            // Verify state after undo
566            prop_assert!(!manager.can_undo(), "Should not be able to undo after undo");
567            prop_assert!(manager.can_redo(), "Should be able to redo after undo");
568
569            // Perform redo
570            let redone = manager.redo().unwrap();
571            prop_assert_eq!(&redone.id, &original_id, "Redone change should have same ID");
572            prop_assert_eq!(&redone.file_path, &file_path, "File path should match");
573            prop_assert_eq!(&redone.before, &before, "Before state should match");
574            prop_assert_eq!(&redone.after, &after, "After state should match");
575
576            // Verify state after redo
577            prop_assert!(manager.can_undo(), "Should be able to undo after redo");
578            prop_assert!(!manager.can_redo(), "Should not be able to redo after redo");
579        }
580    }
581}