use crate::block::Block;
use crate::document::Document;
use crate::error::{BlocksError, Result};
use std::collections::VecDeque;
use uuid::Uuid;
#[derive(Debug, Clone)]
pub enum DocumentOperation {
AddBlock { block: Block, index: usize },
RemoveBlock { block: Block, index: usize },
UpdateBlock {
old_content: String,
new_content: String,
block_id: Uuid,
},
ChangeTitle {
old_title: String,
new_title: String,
},
ClearBlocks { blocks: Vec<Block> },
}
impl DocumentOperation {
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(),
}
}
}
#[derive(Clone)]
pub struct HistoryManager {
undo_stack: VecDeque<DocumentOperation>,
redo_stack: VecDeque<DocumentOperation>,
max_history: usize,
}
impl HistoryManager {
pub fn new(max_history: usize) -> Self {
Self {
undo_stack: VecDeque::with_capacity(max_history),
redo_stack: VecDeque::with_capacity(max_history),
max_history,
}
}
pub fn record(&mut self, operation: DocumentOperation) {
self.undo_stack.push_back(operation);
if self.undo_stack.len() > self.max_history {
self.undo_stack.pop_front();
}
self.redo_stack.clear();
}
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
}
}
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
}
}
pub fn can_undo(&self) -> bool {
!self.undo_stack.is_empty()
}
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
pub fn undo_count(&self) -> usize {
self.undo_stack.len()
}
pub fn redo_count(&self) -> usize {
self.redo_stack.len()
}
pub fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
}
pub fn next_undo_description(&self) -> Option<String> {
self.undo_stack.back().map(|op| op.description())
}
pub fn next_redo_description(&self) -> Option<String> {
self.redo_stack.back().map(|op| op.description())
}
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(())
}
}
}
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);
}
}