ass_editor/core/
history.rs

1//! History management for undo/redo operations
2//!
3//! Provides efficient undo/redo functionality with configurable depth limits,
4//! arena-based memory pooling, and delta compression to minimize memory usage.
5
6use super::position::{Position, Range};
7use crate::commands::CommandResult;
8
9#[cfg(feature = "stream")]
10use ass_core::parser::ScriptDeltaOwned;
11
12#[cfg(feature = "arena")]
13use bumpalo::Bump;
14
15#[cfg(not(feature = "std"))]
16use alloc::{string::String, vec::Vec};
17
18#[cfg(feature = "std")]
19use std::collections::VecDeque;
20
21#[cfg(not(feature = "std"))]
22use alloc::collections::VecDeque;
23
24/// Data needed to undo a delta operation
25#[cfg(feature = "stream")]
26#[derive(Debug, Clone)]
27pub struct DeltaUndoData {
28    /// Sections that were removed (index and content)
29    pub removed_sections: Vec<(usize, String)>,
30    /// Sections that were modified (index and original content)
31    pub modified_sections: Vec<(usize, String)>,
32}
33
34/// Represents an operation that can be undone and redone
35#[derive(Debug, Clone)]
36pub enum Operation {
37    /// Text was inserted
38    Insert { position: Position, text: String },
39    /// Text was deleted
40    Delete { range: Range, deleted_text: String },
41    /// Text was replaced
42    Replace {
43        range: Range,
44        old_text: String,
45        new_text: String,
46    },
47    /// Delta was applied (for incremental updates)
48    #[cfg(feature = "stream")]
49    Delta {
50        /// The forward delta to apply
51        forward: ScriptDeltaOwned,
52        /// Data needed to undo this delta
53        undo_data: DeltaUndoData,
54    },
55}
56
57impl Operation {
58    /// Get memory usage of this operation
59    pub fn memory_usage(&self) -> usize {
60        match self {
61            Self::Insert { text, .. } => core::mem::size_of::<Self>() + text.len(),
62            Self::Delete { deleted_text, .. } => core::mem::size_of::<Self>() + deleted_text.len(),
63            Self::Replace {
64                old_text, new_text, ..
65            } => core::mem::size_of::<Self>() + old_text.len() + new_text.len(),
66            #[cfg(feature = "stream")]
67            Self::Delta { forward, undo_data } => {
68                core::mem::size_of::<Self>()
69                    + forward.added.iter().map(|s| s.len()).sum::<usize>()
70                    + forward.modified.iter().map(|(_, s)| s.len()).sum::<usize>()
71                    + undo_data
72                        .removed_sections
73                        .iter()
74                        .map(|(_, s)| s.len())
75                        .sum::<usize>()
76                    + undo_data
77                        .modified_sections
78                        .iter()
79                        .map(|(_, s)| s.len())
80                        .sum::<usize>()
81            }
82        }
83    }
84}
85
86/// A single entry in the undo/redo history
87///
88/// Stores the operation data needed to undo and redo changes
89/// along with metadata for efficient memory management.
90#[derive(Debug)]
91pub struct HistoryEntry {
92    /// The operation that was performed
93    pub operation: Operation,
94
95    /// Description of the operation
96    pub description: String,
97
98    /// Range that was modified by the operation
99    pub modified_range: Option<Range>,
100
101    /// Cursor position before the operation
102    pub cursor_before: Option<Position>,
103
104    /// Cursor position after the operation
105    pub cursor_after: Option<Position>,
106
107    /// Script delta for efficient incremental parsing (when available)
108    #[cfg(feature = "stream")]
109    pub script_delta: Option<ScriptDeltaOwned>,
110
111    /// Timestamp when the operation was performed
112    #[cfg(feature = "std")]
113    pub timestamp: std::time::Instant,
114
115    /// Memory usage of this entry (for capacity management)
116    pub memory_usage: usize,
117}
118
119impl HistoryEntry {
120    /// Create a new history entry
121    pub fn new(
122        operation: Operation,
123        description: String,
124        result: &CommandResult,
125        cursor_before: Option<Position>,
126    ) -> Self {
127        let memory_usage = operation.memory_usage() + description.len();
128
129        Self {
130            operation,
131            description,
132            modified_range: result.modified_range,
133            cursor_before,
134            cursor_after: result.new_cursor,
135            #[cfg(feature = "stream")]
136            script_delta: None,
137            #[cfg(feature = "std")]
138            timestamp: std::time::Instant::now(),
139            memory_usage,
140        }
141    }
142
143    /// Create a new history entry with delta
144    #[cfg(feature = "stream")]
145    pub fn with_delta(
146        operation: Operation,
147        description: String,
148        result: &CommandResult,
149        cursor_before: Option<Position>,
150        script_delta: ScriptDeltaOwned,
151    ) -> Self {
152        let delta_memory = script_delta.added.iter().map(|s| s.len()).sum::<usize>()
153            + script_delta
154                .modified
155                .iter()
156                .map(|(_, s)| s.len())
157                .sum::<usize>();
158        let memory_usage = operation.memory_usage() + description.len() + delta_memory;
159
160        Self {
161            operation,
162            description,
163            modified_range: result.modified_range,
164            cursor_before,
165            cursor_after: result.new_cursor,
166            script_delta: Some(script_delta),
167            #[cfg(feature = "std")]
168            timestamp: std::time::Instant::now(),
169            memory_usage,
170        }
171    }
172}
173
174/// Configuration for undo stack behavior
175#[derive(Debug, Clone)]
176pub struct UndoStackConfig {
177    /// Maximum number of undo entries to keep
178    pub max_entries: usize,
179
180    /// Maximum memory usage in bytes (0 = unlimited)
181    pub max_memory: usize,
182
183    /// Whether to enable compression of old entries
184    pub enable_compression: bool,
185
186    /// Interval for arena resets (0 = never reset)
187    pub arena_reset_interval: usize,
188}
189
190impl Default for UndoStackConfig {
191    fn default() -> Self {
192        Self {
193            max_entries: 50, // Set a sensible default, can be overridden programmatically
194            max_memory: 10 * 1024 * 1024, // 10MB default
195            enable_compression: true,
196            arena_reset_interval: 100, // Reset arena every 100 operations
197        }
198    }
199}
200
201/// Undo/redo stack with efficient memory management
202///
203/// Uses a circular buffer design with arena allocation for optimal
204/// performance. Supports configurable limits and automatic cleanup.
205#[derive(Debug)]
206pub struct UndoStack {
207    /// Configuration for this stack
208    config: UndoStackConfig,
209
210    /// Undo history (most recent operations first)
211    undo_stack: VecDeque<HistoryEntry>,
212
213    /// Redo history (operations that can be redone)
214    redo_stack: VecDeque<HistoryEntry>,
215
216    /// Current memory usage in bytes
217    current_memory: usize,
218
219    /// Arena for temporary allocations
220    #[cfg(feature = "arena")]
221    arena: Bump,
222
223    /// Number of operations since last arena reset
224    #[cfg(feature = "arena")]
225    ops_since_reset: usize,
226}
227
228impl UndoStack {
229    /// Create a new undo stack with default configuration
230    pub fn new() -> Self {
231        Self::with_config(UndoStackConfig::default())
232    }
233
234    /// Create a new undo stack with custom configuration
235    pub fn with_config(config: UndoStackConfig) -> Self {
236        Self {
237            config,
238            undo_stack: VecDeque::new(),
239            redo_stack: VecDeque::new(),
240            current_memory: 0,
241            #[cfg(feature = "arena")]
242            arena: Bump::new(),
243            #[cfg(feature = "arena")]
244            ops_since_reset: 0,
245        }
246    }
247
248    /// Push a new entry onto the undo stack
249    ///
250    /// This clears the redo stack as new operations invalidate
251    /// previously undone operations.
252    pub fn push(&mut self, entry: HistoryEntry) {
253        // Clear redo stack - new operations invalidate undone changes
254        self.clear_redo_stack();
255
256        // Add memory usage
257        self.current_memory += entry.memory_usage;
258
259        // Add to undo stack
260        self.undo_stack.push_front(entry);
261
262        // Enforce limits
263        self.enforce_limits();
264
265        // Periodic arena reset
266        #[cfg(feature = "arena")]
267        {
268            self.ops_since_reset += 1;
269            if self.config.arena_reset_interval > 0
270                && self.ops_since_reset >= self.config.arena_reset_interval
271            {
272                self.reset_arena();
273            }
274        }
275    }
276
277    /// Pop the most recent entry from the undo stack
278    pub fn pop_undo(&mut self) -> Option<HistoryEntry> {
279        if let Some(entry) = self.undo_stack.pop_front() {
280            self.current_memory -= entry.memory_usage;
281            Some(entry)
282        } else {
283            None
284        }
285    }
286
287    /// Push an entry onto the redo stack
288    pub fn push_redo(&mut self, entry: HistoryEntry) {
289        self.current_memory += entry.memory_usage;
290        self.redo_stack.push_front(entry);
291    }
292
293    /// Pop an entry from the redo stack
294    pub fn pop_redo(&mut self) -> Option<HistoryEntry> {
295        if let Some(entry) = self.redo_stack.pop_front() {
296            self.current_memory -= entry.memory_usage;
297            Some(entry)
298        } else {
299            None
300        }
301    }
302
303    /// Check if undo is available
304    #[must_use]
305    pub fn can_undo(&self) -> bool {
306        !self.undo_stack.is_empty()
307    }
308
309    /// Check if redo is available
310    #[must_use]
311    pub fn can_redo(&self) -> bool {
312        !self.redo_stack.is_empty()
313    }
314
315    /// Get the number of undo entries available
316    #[must_use]
317    pub fn undo_count(&self) -> usize {
318        self.undo_stack.len()
319    }
320
321    /// Get the number of redo entries available
322    #[must_use]
323    pub fn redo_count(&self) -> usize {
324        self.redo_stack.len()
325    }
326
327    /// Get current memory usage in bytes
328    #[must_use]
329    pub fn memory_usage(&self) -> usize {
330        self.current_memory
331    }
332
333    /// Get description of the next undo operation
334    #[must_use]
335    pub fn next_undo_description(&self) -> Option<&str> {
336        self.undo_stack
337            .front()
338            .map(|entry| entry.description.as_str())
339    }
340
341    /// Get description of the next redo operation
342    #[must_use]
343    pub fn next_redo_description(&self) -> Option<&str> {
344        self.redo_stack
345            .front()
346            .map(|entry| entry.description.as_str())
347    }
348
349    /// Clear all history
350    pub fn clear(&mut self) {
351        self.undo_stack.clear();
352        self.redo_stack.clear();
353        self.current_memory = 0;
354
355        #[cfg(feature = "arena")]
356        {
357            self.reset_arena();
358        }
359    }
360
361    /// Clear only the redo stack (called when new operations are performed)
362    fn clear_redo_stack(&mut self) {
363        for entry in self.redo_stack.drain(..) {
364            self.current_memory -= entry.memory_usage;
365        }
366    }
367
368    /// Enforce memory and count limits
369    fn enforce_limits(&mut self) {
370        // Enforce entry count limit
371        while self.undo_stack.len() > self.config.max_entries {
372            if let Some(entry) = self.undo_stack.pop_back() {
373                self.current_memory -= entry.memory_usage;
374            }
375        }
376
377        // Enforce memory limit
378        while self.config.max_memory > 0
379            && self.current_memory > self.config.max_memory
380            && !self.undo_stack.is_empty()
381        {
382            if let Some(entry) = self.undo_stack.pop_back() {
383                self.current_memory -= entry.memory_usage;
384            }
385        }
386    }
387
388    /// Reset the arena allocator to reclaim memory
389    #[cfg(feature = "arena")]
390    fn reset_arena(&mut self) {
391        self.arena.reset();
392        self.ops_since_reset = 0;
393    }
394
395    /// Get arena allocator reference
396    #[cfg(feature = "arena")]
397    #[must_use]
398    pub fn arena(&self) -> &Bump {
399        &self.arena
400    }
401
402    /// Get mutable arena allocator reference
403    #[cfg(feature = "arena")]
404    pub fn arena_mut(&mut self) -> &mut Bump {
405        &mut self.arena
406    }
407}
408
409impl Default for UndoStack {
410    fn default() -> Self {
411        Self::new()
412    }
413}
414
415/// Undo manager that coordinates between commands and history stack
416///
417/// Provides high-level undo/redo operations and manages the creation
418/// of inverse commands for undoing operations.
419#[derive(Debug)]
420pub struct UndoManager {
421    /// The underlying undo stack
422    stack: UndoStack,
423
424    /// Current cursor position for context
425    current_cursor: Option<Position>,
426}
427
428impl UndoManager {
429    /// Create a new undo manager
430    pub fn new() -> Self {
431        Self {
432            stack: UndoStack::new(),
433            current_cursor: None,
434        }
435    }
436
437    /// Create a new undo manager with custom configuration
438    pub fn with_config(config: UndoStackConfig) -> Self {
439        Self {
440            stack: UndoStack::with_config(config),
441            current_cursor: None,
442        }
443    }
444
445    /// Update the configuration of the undo stack
446    pub fn set_config(&mut self, config: UndoStackConfig) {
447        self.stack = UndoStack::with_config(config);
448    }
449
450    /// Update the current cursor position
451    pub fn set_cursor(&mut self, cursor: Option<Position>) {
452        self.current_cursor = cursor;
453    }
454
455    /// Get the current cursor position
456    #[must_use]
457    pub fn cursor_position(&self) -> Option<Position> {
458        self.current_cursor
459    }
460
461    /// Record an operation for undo purposes
462    ///
463    /// Stores the operation data in the history stack.
464    pub fn record_operation(
465        &mut self,
466        operation: Operation,
467        description: String,
468        result: &CommandResult,
469    ) {
470        if result.success && result.content_changed {
471            #[cfg(feature = "stream")]
472            let entry = if let Some(ref delta) = result.script_delta {
473                HistoryEntry::with_delta(
474                    operation,
475                    description,
476                    result,
477                    self.current_cursor,
478                    delta.clone(),
479                )
480            } else {
481                HistoryEntry::new(operation, description, result, self.current_cursor)
482            };
483
484            #[cfg(not(feature = "stream"))]
485            let entry = HistoryEntry::new(operation, description, result, self.current_cursor);
486
487            self.stack.push(entry);
488
489            // Update cursor position
490            if let Some(new_cursor) = result.new_cursor {
491                self.current_cursor = Some(new_cursor);
492            }
493        }
494    }
495
496    /// Check if undo is available
497    #[must_use]
498    pub fn can_undo(&self) -> bool {
499        self.stack.can_undo()
500    }
501
502    /// Check if redo is available
503    #[must_use]
504    pub fn can_redo(&self) -> bool {
505        self.stack.can_redo()
506    }
507
508    /// Get the next undo operation description
509    #[must_use]
510    pub fn next_undo_description(&self) -> Option<&str> {
511        self.stack.next_undo_description()
512    }
513
514    /// Get the next redo operation description
515    #[must_use]
516    pub fn next_redo_description(&self) -> Option<&str> {
517        self.stack.next_redo_description()
518    }
519
520    /// Get history statistics
521    #[must_use]
522    pub fn stats(&self) -> HistoryStats {
523        HistoryStats {
524            undo_count: self.stack.undo_count(),
525            redo_count: self.stack.redo_count(),
526            memory_usage: self.stack.memory_usage(),
527        }
528    }
529
530    /// Clear all history
531    pub fn clear(&mut self) {
532        self.stack.clear();
533    }
534
535    /// Access to underlying stack for advanced operations
536    #[must_use]
537    pub fn stack(&self) -> &UndoStack {
538        &self.stack
539    }
540
541    /// Mutable access to underlying stack
542    pub fn stack_mut(&mut self) -> &mut UndoStack {
543        &mut self.stack
544    }
545
546    /// Pop an entry from undo stack and return it
547    pub fn pop_undo_entry(&mut self) -> Option<HistoryEntry> {
548        self.stack.pop_undo()
549    }
550
551    /// Pop an entry from redo stack and return it
552    pub fn pop_redo_entry(&mut self) -> Option<HistoryEntry> {
553        self.stack.pop_redo()
554    }
555
556    /// Push an entry to redo stack
557    pub fn push_redo_entry(&mut self, entry: HistoryEntry) {
558        self.stack.push_redo(entry);
559    }
560}
561
562impl Default for UndoManager {
563    fn default() -> Self {
564        Self::new()
565    }
566}
567
568/// Statistics about the history system
569#[derive(Debug, Clone, PartialEq, Eq)]
570pub struct HistoryStats {
571    /// Number of operations that can be undone
572    pub undo_count: usize,
573    /// Number of operations that can be redone
574    pub redo_count: usize,
575    /// Current memory usage in bytes
576    pub memory_usage: usize,
577}
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582    #[cfg(not(feature = "std"))]
583    use alloc::format;
584    #[cfg(not(feature = "std"))]
585    use alloc::string::ToString;
586
587    #[test]
588    fn undo_stack_basic_operations() {
589        let mut stack = UndoStack::new();
590        assert!(!stack.can_undo());
591        assert!(!stack.can_redo());
592
593        // Create a dummy history entry
594        let operation = Operation::Insert {
595            position: Position::new(0),
596            text: "test".to_string(),
597        };
598        let result = CommandResult::success();
599        let entry = HistoryEntry::new(operation, "Test".to_string(), &result, None);
600
601        stack.push(entry);
602        assert!(stack.can_undo());
603        assert!(!stack.can_redo());
604
605        let popped = stack.pop_undo().unwrap();
606        assert_eq!(popped.description, "Test");
607        assert!(!stack.can_undo());
608    }
609
610    #[test]
611    fn undo_stack_memory_limits() {
612        let config = UndoStackConfig {
613            max_entries: 2,
614            max_memory: 1000,
615            enable_compression: false,
616            arena_reset_interval: 0,
617        };
618
619        let mut stack = UndoStack::with_config(config);
620
621        // Add entries beyond limit
622        for i in 0..5 {
623            let operation = Operation::Insert {
624                position: Position::new(0),
625                text: format!("test{i}"),
626            };
627            let result = CommandResult::success();
628            let entry = HistoryEntry::new(operation, format!("Test {i}"), &result, None);
629            stack.push(entry);
630        }
631
632        // Should be limited to 2 entries
633        assert_eq!(stack.undo_count(), 2);
634    }
635
636    #[test]
637    fn undo_manager_operations() {
638        let mut manager = UndoManager::new();
639
640        // Simulate an insert operation
641        let operation = Operation::Insert {
642            position: Position::new(0),
643            text: "Hello".to_string(),
644        };
645        let mut result = CommandResult::success();
646        result.content_changed = true;
647
648        // Record the operation
649        manager.record_operation(operation, "Insert text".to_string(), &result);
650
651        assert!(manager.can_undo());
652        assert_eq!(manager.next_undo_description(), Some("Insert text"));
653    }
654
655    #[test]
656    fn history_stats() {
657        let manager = UndoManager::new();
658        let stats = manager.stats();
659
660        assert_eq!(stats.undo_count, 0);
661        assert_eq!(stats.redo_count, 0);
662        assert_eq!(stats.memory_usage, 0);
663    }
664
665    #[test]
666    fn operation_memory_usage() {
667        let insert_op = Operation::Insert {
668            position: Position::new(0),
669            text: "Hello".to_string(),
670        };
671        assert!(insert_op.memory_usage() >= 5);
672
673        let delete_op = Operation::Delete {
674            range: Range::new(Position::new(0), Position::new(5)),
675            deleted_text: "Hello".to_string(),
676        };
677        assert!(delete_op.memory_usage() >= 5);
678
679        let replace_op = Operation::Replace {
680            range: Range::new(Position::new(0), Position::new(5)),
681            old_text: "Hello".to_string(),
682            new_text: "World".to_string(),
683        };
684        assert!(replace_op.memory_usage() >= 10);
685    }
686
687    #[test]
688    fn programmatic_undo_limit_configuration() {
689        // Test custom undo limit configuration
690        let custom_config = UndoStackConfig {
691            max_entries: 3,
692            max_memory: 1000,
693            enable_compression: false,
694            arena_reset_interval: 0,
695        };
696
697        let mut manager = UndoManager::with_config(custom_config);
698
699        // Add more operations than the limit
700        for i in 0..5 {
701            let operation = Operation::Insert {
702                position: Position::new(i * 10),
703                text: format!("test{i}"),
704            };
705            let mut result = CommandResult::success();
706            result.content_changed = true;
707            manager.record_operation(operation, format!("Insert {i}"), &result);
708        }
709
710        // Should only keep the last 3 operations due to max_entries limit
711        let stats = manager.stats();
712        assert_eq!(stats.undo_count, 3);
713
714        // Check that the correct operations are kept (most recent ones)
715        assert_eq!(manager.next_undo_description(), Some("Insert 4"));
716
717        // Test undo operations respect the limit
718        manager.pop_undo_entry();
719        assert_eq!(manager.next_undo_description(), Some("Insert 3"));
720
721        manager.pop_undo_entry();
722        assert_eq!(manager.next_undo_description(), Some("Insert 2"));
723
724        manager.pop_undo_entry();
725        assert_eq!(manager.next_undo_description(), None);
726    }
727
728    #[test]
729    fn undo_stack_config_default() {
730        // Test that default configuration is sensible
731        let default_config = UndoStackConfig::default();
732        assert_eq!(default_config.max_entries, 50);
733        assert_eq!(default_config.max_memory, 10 * 1024 * 1024); // 10MB
734        assert!(default_config.enable_compression);
735        assert_eq!(default_config.arena_reset_interval, 100);
736    }
737
738    #[test]
739    fn undo_manager_config_update() {
740        // Test that UndoManager can have its configuration updated
741        let mut manager = UndoManager::new();
742
743        // Add an operation with default config
744        let operation = Operation::Insert {
745            position: Position::new(0),
746            text: "test".to_string(),
747        };
748        let mut result = CommandResult::success();
749        result.content_changed = true;
750        manager.record_operation(operation, "Initial".to_string(), &result);
751
752        assert_eq!(manager.stats().undo_count, 1);
753
754        // Update to a more restrictive config
755        let restrictive_config = UndoStackConfig {
756            max_entries: 0, // No undo history allowed
757            max_memory: 0,
758            enable_compression: false,
759            arena_reset_interval: 0,
760        };
761
762        manager.set_config(restrictive_config);
763
764        // The stack should be recreated, so previous operations should be gone
765        assert_eq!(manager.stats().undo_count, 0);
766
767        // New operations should not be recorded due to 0 limit
768        let operation = Operation::Insert {
769            position: Position::new(5),
770            text: "test2".to_string(),
771        };
772        manager.record_operation(operation, "Should not record".to_string(), &result);
773        assert_eq!(manager.stats().undo_count, 0);
774    }
775
776    #[test]
777    fn memory_limit_enforcement() {
778        // Test that memory limits are enforced
779        let memory_limited_config = UndoStackConfig {
780            max_entries: 100, // High entry limit
781            max_memory: 50,   // Very low memory limit (50 bytes)
782            enable_compression: false,
783            arena_reset_interval: 0,
784        };
785
786        let mut manager = UndoManager::with_config(memory_limited_config);
787
788        // Add operations that exceed memory limit
789        for i in 0..5 {
790            let operation = Operation::Insert {
791                position: Position::new(i * 10),
792                text: format!(
793                    "This is a long text string for operation {i} that should consume memory"
794                ),
795            };
796            let mut result = CommandResult::success();
797            result.content_changed = true;
798            manager.record_operation(operation, format!("Long operation {i}"), &result);
799        }
800
801        // Should have fewer operations due to memory constraint
802        let stats = manager.stats();
803        assert!(stats.undo_count < 5);
804        assert!(stats.memory_usage <= 50 || stats.undo_count == 0);
805    }
806}