blocks 0.1.0

A high-performance Rust library for block-based content editing with JSON, Markdown, and HTML support
Documentation
/// History module for undo/redo functionality
///
/// Provides a command pattern-based history system for tracking and reversing document changes.
use crate::block::Block;
use crate::document::Document;
use crate::error::{BlocksError, Result};
use std::collections::VecDeque;
use uuid::Uuid;

/// Represents a single operation that can be undone/redone
#[derive(Debug, Clone)]
pub enum DocumentOperation {
    /// Adding a block
    AddBlock { block: Block, index: usize },
    /// Removing a block
    RemoveBlock { block: Block, index: usize },
    /// Updating block content
    UpdateBlock {
        old_content: String,
        new_content: String,
        block_id: Uuid,
    },
    /// Changing document title
    ChangeTitle {
        old_title: String,
        new_title: String,
    },
    /// Clearing all blocks
    ClearBlocks { blocks: Vec<Block> },
}

impl DocumentOperation {
    /// Returns a description of the operation for UI display
    pub fn description(&self) -> String {
        match self {
            Self::AddBlock { .. } => "Add block".to_string(),
            Self::RemoveBlock { .. } => "Remove block".to_string(),
            Self::UpdateBlock { .. } => "Update block".to_string(),
            Self::ChangeTitle { .. } => "Change title".to_string(),
            Self::ClearBlocks { .. } => "Clear blocks".to_string(),
        }
    }
}

/// History manager for tracking document changes
///
/// # Example
///
/// ```rust
/// use blocks::history::HistoryManager;
/// use blocks::Document;
///
/// let mut history = HistoryManager::new(50);
/// // Perform operations...
/// // later:
/// history.undo(); // Revert last change
/// history.redo(); // Redo the change
/// ```
#[derive(Clone)]
pub struct HistoryManager {
    undo_stack: VecDeque<DocumentOperation>,
    redo_stack: VecDeque<DocumentOperation>,
    max_history: usize,
}

impl HistoryManager {
    /// Creates a new history manager with a maximum number of operations
    ///
    /// # Arguments
    ///
    /// * `max_history` - Maximum number of operations to keep in history (default: 50)
    pub fn new(max_history: usize) -> Self {
        Self {
            undo_stack: VecDeque::with_capacity(max_history),
            redo_stack: VecDeque::with_capacity(max_history),
            max_history,
        }
    }

    /// Records an operation in the history
    ///
    /// Clears the redo stack when a new operation is recorded
    pub fn record(&mut self, operation: DocumentOperation) {
        self.undo_stack.push_back(operation);

        // Trim history if it exceeds max size
        if self.undo_stack.len() > self.max_history {
            self.undo_stack.pop_front();
        }

        // Clear redo stack when a new operation is recorded
        self.redo_stack.clear();
    }

    /// Undoes the last operation
    ///
    /// Returns the operation that was undone, or None if there's nothing to undo
    pub fn undo(&mut self) -> Option<DocumentOperation> {
        if let Some(operation) = self.undo_stack.pop_back() {
            self.redo_stack.push_back(operation.clone());
            Some(operation)
        } else {
            None
        }
    }

    /// Redoes the last undone operation
    ///
    /// Returns the operation that was redone, or None if there's nothing to redo
    pub fn redo(&mut self) -> Option<DocumentOperation> {
        if let Some(operation) = self.redo_stack.pop_back() {
            self.undo_stack.push_back(operation.clone());
            Some(operation)
        } else {
            None
        }
    }

    /// Checks if there's an operation to undo
    pub fn can_undo(&self) -> bool {
        !self.undo_stack.is_empty()
    }

    /// Checks if there's an operation to redo
    pub fn can_redo(&self) -> bool {
        !self.redo_stack.is_empty()
    }

    /// Returns the number of operations in the undo stack
    pub fn undo_count(&self) -> usize {
        self.undo_stack.len()
    }

    /// Returns the number of operations in the redo stack
    pub fn redo_count(&self) -> usize {
        self.redo_stack.len()
    }

    /// Clears all undo and redo history
    pub fn clear(&mut self) {
        self.undo_stack.clear();
        self.redo_stack.clear();
    }

    /// Gets the description of the next undo operation
    pub fn next_undo_description(&self) -> Option<String> {
        self.undo_stack.back().map(|op| op.description())
    }

    /// Gets the description of the next redo operation
    pub fn next_redo_description(&self) -> Option<String> {
        self.redo_stack.back().map(|op| op.description())
    }

    /// Applies an undo operation to the document
    pub fn apply_undo(&self, doc: &mut Document, operation: &DocumentOperation) -> Result<()> {
        match operation {
            DocumentOperation::AddBlock { index, .. } => {
                if *index < doc.blocks.len() {
                    doc.blocks.remove(*index);
                    doc.update_timestamp();
                    Ok(())
                } else {
                    Err(BlocksError::ValidationError {
                        message: format!("Invalid block index: {}", index),
                    })
                }
            }
            DocumentOperation::RemoveBlock { block, index } => {
                if *index <= doc.blocks.len() {
                    doc.blocks.insert(*index, block.clone());
                    doc.update_timestamp();
                    Ok(())
                } else {
                    Err(BlocksError::ValidationError {
                        message: format!("Cannot insert at index: {}", index),
                    })
                }
            }
            DocumentOperation::UpdateBlock {
                old_content,
                block_id,
                ..
            } => {
                for block in &mut doc.blocks {
                    if block.id == *block_id {
                        block.content = old_content.clone();
                        block.update_timestamp();
                        doc.update_timestamp();
                        return Ok(());
                    }
                }
                Err(BlocksError::ValidationError {
                    message: format!("Block not found: {}", block_id),
                })
            }
            DocumentOperation::ChangeTitle { old_title, .. } => {
                doc.title = old_title.clone();
                doc.update_timestamp();
                Ok(())
            }
            DocumentOperation::ClearBlocks { blocks } => {
                doc.blocks = blocks.clone();
                doc.update_timestamp();
                Ok(())
            }
        }
    }

    /// Applies a redo operation to the document
    pub fn apply_redo(&self, doc: &mut Document, operation: &DocumentOperation) -> Result<()> {
        match operation {
            DocumentOperation::AddBlock { block, index } => {
                if *index <= doc.blocks.len() {
                    doc.blocks.insert(*index, block.clone());
                    doc.update_timestamp();
                    Ok(())
                } else {
                    Err(BlocksError::ValidationError {
                        message: format!("Cannot insert at index: {}", index),
                    })
                }
            }
            DocumentOperation::RemoveBlock { index, .. } => {
                if *index < doc.blocks.len() {
                    doc.blocks.remove(*index);
                    doc.update_timestamp();
                    Ok(())
                } else {
                    Err(BlocksError::ValidationError {
                        message: format!("Invalid block index: {}", index),
                    })
                }
            }
            DocumentOperation::UpdateBlock {
                new_content,
                block_id,
                ..
            } => {
                for block in &mut doc.blocks {
                    if block.id == *block_id {
                        block.content = new_content.clone();
                        block.update_timestamp();
                        doc.update_timestamp();
                        return Ok(());
                    }
                }
                Err(BlocksError::ValidationError {
                    message: format!("Block not found: {}", block_id),
                })
            }
            DocumentOperation::ChangeTitle { new_title, .. } => {
                doc.title = new_title.clone();
                doc.update_timestamp();
                Ok(())
            }
            DocumentOperation::ClearBlocks { .. } => {
                doc.blocks.clear();
                doc.update_timestamp();
                Ok(())
            }
        }
    }
}

impl Default for HistoryManager {
    fn default() -> Self {
        Self::new(50)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_history_manager_creation() {
        let history = HistoryManager::new(100);
        assert_eq!(history.undo_count(), 0);
        assert_eq!(history.redo_count(), 0);
        assert!(!history.can_undo());
        assert!(!history.can_redo());
    }

    #[test]
    fn test_record_and_undo() {
        let mut history = HistoryManager::new(50);
        let op = DocumentOperation::ChangeTitle {
            old_title: "Old".to_string(),
            new_title: "New".to_string(),
        };

        history.record(op);
        assert_eq!(history.undo_count(), 1);
        assert!(!history.can_redo());

        let undone = history.undo();
        assert!(undone.is_some());
        assert_eq!(history.undo_count(), 0);
        assert!(history.can_redo());
    }

    #[test]
    fn test_undo_and_redo() {
        let mut history = HistoryManager::new(50);
        let op = DocumentOperation::ChangeTitle {
            old_title: "Old".to_string(),
            new_title: "New".to_string(),
        };

        history.record(op);
        history.undo();
        history.redo();

        assert_eq!(history.undo_count(), 1);
        assert_eq!(history.redo_count(), 0);
    }

    #[test]
    fn test_new_operation_clears_redo() {
        let mut history = HistoryManager::new(50);
        let op1 = DocumentOperation::ChangeTitle {
            old_title: "Old".to_string(),
            new_title: "New".to_string(),
        };

        history.record(op1);
        history.undo();
        assert!(history.can_redo());

        let op2 = DocumentOperation::ChangeTitle {
            old_title: "New".to_string(),
            new_title: "Newer".to_string(),
        };

        history.record(op2);
        assert!(!history.can_redo());
    }

    #[test]
    fn test_max_history_limit() {
        let mut history = HistoryManager::new(3);

        for i in 0..5 {
            let op = DocumentOperation::ChangeTitle {
                old_title: format!("Title {}", i),
                new_title: format!("Title {}", i + 1),
            };
            history.record(op);
        }

        assert_eq!(history.undo_count(), 3);
    }

    #[test]
    fn test_descriptions() {
        let mut history = HistoryManager::new(50);
        let op = DocumentOperation::AddBlock {
            block: Block::new(crate::BlockType::Text, "test".to_string()),
            index: 0,
        };

        history.record(op);
        assert_eq!(
            history.next_undo_description(),
            Some("Add block".to_string())
        );
    }

    #[test]
    fn test_clear() {
        let mut history = HistoryManager::new(50);
        let op = DocumentOperation::ChangeTitle {
            old_title: "Old".to_string(),
            new_title: "New".to_string(),
        };

        history.record(op);
        assert!(history.can_undo());

        history.clear();
        assert!(!history.can_undo());
        assert_eq!(history.undo_count(), 0);
    }
}