#![allow(clippy::unwrap_used)]
#![allow(clippy::missing_panics_doc)]
use super::command::{Command, CompositeCommand};
use crate::text_buffer::TextBuffer;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone)]
pub struct CommandHistory {
inner: Arc<Mutex<HistoryInner>>,
}
#[derive(Debug)]
struct HistoryInner {
undo_stack: Vec<Box<dyn Command>>,
redo_stack: Vec<Box<dyn Command>>,
max_size: usize,
save_point: Option<usize>,
current_group: Option<CompositeCommand>,
}
impl CommandHistory {
pub fn new(max_size: usize) -> Self {
Self {
inner: Arc::new(Mutex::new(HistoryInner {
undo_stack: Vec::with_capacity(max_size.min(100)),
redo_stack: Vec::with_capacity(max_size.min(100)),
max_size,
save_point: None,
current_group: None,
})),
}
}
pub fn push(&self, command: Box<dyn Command>) {
let mut inner = self.inner.lock().unwrap();
if let Some(ref mut group) = inner.current_group {
group.add(command);
return;
}
inner.redo_stack.clear();
inner.undo_stack.push(command);
if inner.undo_stack.len() > inner.max_size {
inner.undo_stack.remove(0);
if let Some(ref mut sp) = inner.save_point {
if *sp > 0 {
*sp -= 1;
} else {
inner.save_point = None;
}
}
}
}
pub fn undo(
&self,
buffer: &mut TextBuffer,
cursor: &mut (usize, usize),
) -> bool {
let mut inner = self.inner.lock().unwrap();
if inner.current_group.is_some() {
Self::end_group_internal(&mut inner);
}
if let Some(mut command) = inner.undo_stack.pop() {
command.undo(buffer, cursor);
inner.redo_stack.push(command);
true
} else {
false
}
}
pub fn redo(
&self,
buffer: &mut TextBuffer,
cursor: &mut (usize, usize),
) -> bool {
let mut inner = self.inner.lock().unwrap();
if let Some(mut command) = inner.redo_stack.pop() {
command.execute(buffer, cursor);
inner.undo_stack.push(command);
true
} else {
false
}
}
#[must_use]
pub fn can_undo(&self) -> bool {
let inner = self.inner.lock().unwrap();
!inner.undo_stack.is_empty() || inner.current_group.is_some()
}
#[must_use]
pub fn can_redo(&self) -> bool {
let inner = self.inner.lock().unwrap();
!inner.redo_stack.is_empty()
}
pub fn mark_saved(&self) {
let mut inner = self.inner.lock().unwrap();
inner.save_point = Some(inner.undo_stack.len());
}
#[must_use]
pub fn is_modified(&self) -> bool {
let inner = self.inner.lock().unwrap();
if inner.current_group.is_some() {
return true;
}
match inner.save_point {
None => !inner.undo_stack.is_empty(),
Some(sp) => sp != inner.undo_stack.len(),
}
}
pub fn clear(&self) {
let mut inner = self.inner.lock().unwrap();
inner.undo_stack.clear();
inner.redo_stack.clear();
inner.save_point = None;
inner.current_group = None;
}
pub fn begin_group(&self, description: &str) {
let mut inner = self.inner.lock().unwrap();
if inner.current_group.is_none() {
inner.current_group =
Some(CompositeCommand::new(description.to_string()));
}
}
pub fn end_group(&self) {
let mut inner = self.inner.lock().unwrap();
Self::end_group_internal(&mut inner);
}
fn end_group_internal(inner: &mut HistoryInner) {
if let Some(group) = inner.current_group.take()
&& !group.is_empty()
{
inner.redo_stack.clear();
inner.undo_stack.push(Box::new(group));
if inner.undo_stack.len() > inner.max_size {
inner.undo_stack.remove(0);
if let Some(ref mut sp) = inner.save_point {
if *sp > 0 {
*sp -= 1;
} else {
inner.save_point = None;
}
}
}
}
}
#[must_use]
pub fn max_size(&self) -> usize {
let inner = self.inner.lock().unwrap();
inner.max_size
}
pub fn set_max_size(&self, max_size: usize) {
let mut inner = self.inner.lock().unwrap();
inner.max_size = max_size;
while inner.undo_stack.len() > max_size {
inner.undo_stack.remove(0);
if let Some(ref mut sp) = inner.save_point {
if *sp > 0 {
*sp -= 1;
} else {
inner.save_point = None;
}
}
}
}
#[must_use]
pub fn undo_count(&self) -> usize {
let inner = self.inner.lock().unwrap();
inner.undo_stack.len()
}
#[must_use]
pub fn redo_count(&self) -> usize {
let inner = self.inner.lock().unwrap();
inner.redo_stack.len()
}
}
impl Default for CommandHistory {
fn default() -> Self {
Self::new(100)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::canvas_editor::command::InsertCharCommand;
#[test]
fn test_new_history() {
let history = CommandHistory::new(50);
assert_eq!(history.max_size(), 50);
assert!(!history.can_undo());
assert!(!history.can_redo());
}
#[test]
fn test_push_and_undo() {
let mut buffer = TextBuffer::new("hello");
let mut cursor = (0, 5);
let history = CommandHistory::new(10);
let mut cmd = InsertCharCommand::new(0, 5, '!', cursor);
cmd.execute(&mut buffer, &mut cursor);
history.push(Box::new(cmd));
assert!(history.can_undo());
assert_eq!(buffer.line(0), "hello!");
history.undo(&mut buffer, &mut cursor);
assert_eq!(buffer.line(0), "hello");
assert_eq!(cursor, (0, 5));
}
#[test]
fn test_redo() {
let mut buffer = TextBuffer::new("hello");
let mut cursor = (0, 5);
let history = CommandHistory::new(10);
let mut cmd = InsertCharCommand::new(0, 5, '!', cursor);
cmd.execute(&mut buffer, &mut cursor);
history.push(Box::new(cmd));
history.undo(&mut buffer, &mut cursor);
assert_eq!(buffer.line(0), "hello");
assert!(history.can_redo());
history.redo(&mut buffer, &mut cursor);
assert_eq!(buffer.line(0), "hello!");
assert_eq!(cursor, (0, 6));
}
#[test]
fn test_save_point() {
let mut buffer = TextBuffer::new("hello");
let mut cursor = (0, 5);
let history = CommandHistory::new(10);
assert!(!history.is_modified());
let mut cmd = InsertCharCommand::new(0, 5, '!', cursor);
cmd.execute(&mut buffer, &mut cursor);
history.push(Box::new(cmd));
assert!(history.is_modified());
history.mark_saved();
assert!(!history.is_modified());
let mut cmd2 = InsertCharCommand::new(0, 6, '?', cursor);
cmd2.execute(&mut buffer, &mut cursor);
history.push(Box::new(cmd2));
assert!(history.is_modified()); }
#[test]
fn test_clear() {
let mut buffer = TextBuffer::new("hello");
let mut cursor = (0, 5);
let history = CommandHistory::new(10);
let mut cmd = InsertCharCommand::new(0, 5, '!', cursor);
cmd.execute(&mut buffer, &mut cursor);
history.push(Box::new(cmd));
assert!(history.can_undo());
history.clear();
assert!(!history.can_undo());
assert!(!history.is_modified());
}
#[test]
fn test_size_limit() {
let mut buffer = TextBuffer::new("a");
let mut cursor = (0, 1);
let history = CommandHistory::new(3);
for i in 0..5 {
let mut cmd = InsertCharCommand::new(0, 1 + i, 'x', cursor);
cmd.execute(&mut buffer, &mut cursor);
cursor.1 += 1;
history.push(Box::new(cmd));
}
assert_eq!(history.undo_count(), 3);
}
#[test]
fn test_grouping() {
let mut buffer = TextBuffer::new("hello");
let mut cursor = (0, 5);
let history = CommandHistory::new(10);
history.begin_group("typing");
for ch in "!!!".chars() {
let mut cmd = InsertCharCommand::new(0, cursor.1, ch, cursor);
cmd.execute(&mut buffer, &mut cursor);
history.push(Box::new(cmd));
}
history.end_group();
assert_eq!(buffer.line(0), "hello!!!");
assert_eq!(history.undo_count(), 1);
history.undo(&mut buffer, &mut cursor);
assert_eq!(buffer.line(0), "hello");
assert_eq!(cursor, (0, 5));
}
#[test]
fn test_push_clears_redo() {
let mut buffer = TextBuffer::new("hello");
let mut cursor = (0, 5);
let history = CommandHistory::new(10);
let mut cmd1 = InsertCharCommand::new(0, 5, '!', cursor);
cmd1.execute(&mut buffer, &mut cursor);
history.push(Box::new(cmd1));
history.undo(&mut buffer, &mut cursor);
assert!(history.can_redo());
let mut cmd2 = InsertCharCommand::new(0, 5, '?', cursor);
cmd2.execute(&mut buffer, &mut cursor);
history.push(Box::new(cmd2));
assert!(!history.can_redo());
}
}