eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Undo/Redo stack for state history navigation.
//!
//! Provides ability to undo and redo actions with a fixed-size history.

use std::collections::VecDeque;

/// An undoable action with its reverse.
#[derive(Debug, Clone)]
pub struct UndoableAction {
    /// Description of the action
    pub description: String,
    /// Msg to dispatch to undo
    pub undo_msg: String,
    /// Msg to dispatch to redo
    pub redo_msg: String,
}

/// Undo/Redo stack.
pub struct UndoStack {
    /// Past actions (can be undone)
    undo_stack: VecDeque<UndoableAction>,
    /// Future actions (can be redone)
    redo_stack: VecDeque<UndoableAction>,
    /// Maximum history size
    max_size: usize,
}

impl UndoStack {
    /// Create a new undo stack.
    pub fn new(max_size: usize) -> Self {
        Self {
            undo_stack: VecDeque::with_capacity(max_size),
            redo_stack: VecDeque::with_capacity(max_size),
            max_size,
        }
    }
    
    /// Push a new undoable action.
    pub fn push(&mut self, action: UndoableAction) {
        // Clear redo stack on new action
        self.redo_stack.clear();
        
        // Evict oldest if at capacity
        if self.undo_stack.len() >= self.max_size {
            self.undo_stack.pop_front();
        }
        
        self.undo_stack.push_back(action);
    }
    
    /// Undo the last action, returns the undo message.
    pub fn undo(&mut self) -> Option<String> {
        if let Some(action) = self.undo_stack.pop_back() {
            let msg = action.undo_msg.clone();
            self.redo_stack.push_back(action);
            Some(msg)
        } else {
            None
        }
    }
    
    /// Redo the last undone action, returns the redo message.
    pub fn redo(&mut self) -> Option<String> {
        if let Some(action) = self.redo_stack.pop_back() {
            let msg = action.redo_msg.clone();
            self.undo_stack.push_back(action);
            Some(msg)
        } else {
            None
        }
    }
    
    /// Check if undo is available.
    pub fn can_undo(&self) -> bool {
        !self.undo_stack.is_empty()
    }
    
    /// Check if redo is available.
    pub fn can_redo(&self) -> bool {
        !self.redo_stack.is_empty()
    }
    
    /// Get undo count.
    pub fn undo_count(&self) -> usize {
        self.undo_stack.len()
    }
    
    /// Get redo count.
    pub fn redo_count(&self) -> usize {
        self.redo_stack.len()
    }
    
    /// Get last action description.
    pub fn last_action(&self) -> Option<&str> {
        self.undo_stack.back().map(|a| a.description.as_str())
    }
    
    /// Clear all history.
    pub fn clear(&mut self) {
        self.undo_stack.clear();
        self.redo_stack.clear();
    }
}

impl Default for UndoStack {
    fn default() -> Self {
        Self::new(50) // Keep 50 actions
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    fn make_action(name: &str) -> UndoableAction {
        UndoableAction {
            description: name.to_string(),
            undo_msg: format!("undo_{}", name),
            redo_msg: format!("redo_{}", name),
        }
    }
    
    #[test]
    fn test_undo_redo() {
        let mut stack = UndoStack::new(10);
        
        stack.push(make_action("stage"));
        assert!(stack.can_undo());
        assert!(!stack.can_redo());
        
        let undo = stack.undo();
        assert_eq!(undo, Some("undo_stage".to_string()));
        assert!(!stack.can_undo());
        assert!(stack.can_redo());
        
        let redo = stack.redo();
        assert_eq!(redo, Some("redo_stage".to_string()));
    }
    
    #[test]
    fn test_new_action_clears_redo() {
        let mut stack = UndoStack::new(10);
        
        stack.push(make_action("a"));
        stack.push(make_action("b"));
        stack.undo(); // Can redo "b"
        
        assert!(stack.can_redo());
        
        stack.push(make_action("c")); // Should clear redo
        assert!(!stack.can_redo());
    }
}