#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub struct CommandRecord {
pub name: String,
pub timestamp: f64,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CommandHistoryConfig {
pub max_depth: usize,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CommandHistory {
pub config: CommandHistoryConfig,
pub entries: Vec<CommandRecord>,
pub cursor: usize,
}
#[allow(dead_code)]
pub fn default_command_history_config() -> CommandHistoryConfig {
CommandHistoryConfig { max_depth: 0 }
}
#[allow(dead_code)]
pub fn new_command_history(cfg: &CommandHistoryConfig) -> CommandHistory {
CommandHistory {
config: cfg.clone(),
entries: Vec::new(),
cursor: 0,
}
}
#[allow(dead_code)]
pub fn history_push(history: &mut CommandHistory, name: &str, timestamp: f64) {
history.entries.truncate(history.cursor);
history.entries.push(CommandRecord {
name: name.to_string(),
timestamp,
});
history.cursor = history.entries.len();
let max = history.config.max_depth;
if max > 0 && history.cursor > max {
let trim = history.cursor - max;
history.entries.drain(0..trim);
history.cursor = history.entries.len();
}
}
#[allow(dead_code)]
pub fn history_undo(history: &mut CommandHistory) -> Option<CommandRecord> {
if history.cursor == 0 {
return None;
}
history.cursor -= 1;
Some(history.entries[history.cursor].clone())
}
#[allow(dead_code)]
pub fn history_redo(history: &mut CommandHistory) -> Option<CommandRecord> {
if history.cursor >= history.entries.len() {
return None;
}
let record = history.entries[history.cursor].clone();
history.cursor += 1;
Some(record)
}
#[allow(dead_code)]
pub fn history_can_undo(history: &CommandHistory) -> bool {
history.cursor > 0
}
#[allow(dead_code)]
pub fn history_can_redo(history: &CommandHistory) -> bool {
history.cursor < history.entries.len()
}
#[allow(dead_code)]
pub fn history_entry_count(history: &CommandHistory) -> usize {
history.entries.len()
}
#[allow(dead_code)]
pub fn history_clear(history: &mut CommandHistory) {
history.entries.clear();
history.cursor = 0;
}
#[allow(dead_code)]
pub fn history_current_command(history: &CommandHistory) -> Option<&CommandRecord> {
if history.cursor == 0 {
return None;
}
Some(&history.entries[history.cursor - 1])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let cfg = default_command_history_config();
assert_eq!(cfg.max_depth, 0);
}
#[test]
fn test_new_history_empty() {
let cfg = default_command_history_config();
let history = new_command_history(&cfg);
assert_eq!(history_entry_count(&history), 0);
assert!(!history_can_undo(&history));
assert!(!history_can_redo(&history));
}
#[test]
fn test_push_and_can_undo() {
let cfg = default_command_history_config();
let mut history = new_command_history(&cfg);
history_push(&mut history, "move", 1.0);
assert!(history_can_undo(&history));
assert!(!history_can_redo(&history));
assert_eq!(history_entry_count(&history), 1);
}
#[test]
fn test_undo_returns_record() {
let cfg = default_command_history_config();
let mut history = new_command_history(&cfg);
history_push(&mut history, "scale", 2.0);
let rec = history_undo(&mut history).expect("should succeed");
assert_eq!(rec.name, "scale");
assert!((rec.timestamp - 2.0).abs() < 1e-9);
assert!(!history_can_undo(&history));
assert!(history_can_redo(&history));
}
#[test]
fn test_redo() {
let cfg = default_command_history_config();
let mut history = new_command_history(&cfg);
history_push(&mut history, "rotate", 3.0);
history_undo(&mut history);
let rec = history_redo(&mut history).expect("should succeed");
assert_eq!(rec.name, "rotate");
assert!(history_can_undo(&history));
assert!(!history_can_redo(&history));
}
#[test]
fn test_push_discards_redo_branch() {
let cfg = default_command_history_config();
let mut history = new_command_history(&cfg);
history_push(&mut history, "A", 1.0);
history_push(&mut history, "B", 2.0);
history_undo(&mut history); history_push(&mut history, "C", 3.0); assert_eq!(history_entry_count(&history), 2); assert!(!history_can_redo(&history));
}
#[test]
fn test_max_depth() {
let cfg = CommandHistoryConfig { max_depth: 3 };
let mut history = new_command_history(&cfg);
for i in 0..5_u32 {
history_push(&mut history, &format!("cmd{i}"), f64::from(i));
}
assert_eq!(history_entry_count(&history), 3);
assert_eq!(history.entries[0].name, "cmd2");
}
#[test]
fn test_clear() {
let cfg = default_command_history_config();
let mut history = new_command_history(&cfg);
history_push(&mut history, "x", 1.0);
history_push(&mut history, "y", 2.0);
history_clear(&mut history);
assert_eq!(history_entry_count(&history), 0);
assert!(!history_can_undo(&history));
assert!(!history_can_redo(&history));
}
#[test]
fn test_current_command() {
let cfg = default_command_history_config();
let mut history = new_command_history(&cfg);
assert!(history_current_command(&history).is_none());
history_push(&mut history, "paint", 5.0);
assert_eq!(history_current_command(&history).expect("should succeed").name, "paint");
history_undo(&mut history);
assert!(history_current_command(&history).is_none());
}
#[test]
fn test_undo_none_when_empty() {
let cfg = default_command_history_config();
let mut history = new_command_history(&cfg);
assert!(history_undo(&mut history).is_none());
}
#[test]
fn test_redo_none_when_no_future() {
let cfg = default_command_history_config();
let mut history = new_command_history(&cfg);
history_push(&mut history, "A", 0.0);
assert!(history_redo(&mut history).is_none());
}
}