use std::sync::{Arc, RwLock};
use crate::fiber::FiberId;
use crate::fiber_tree::with_current_fiber;
use crate::scheduler::batch::{StateUpdate, StateUpdateKind, queue_update};
#[derive(Clone)]
struct HistoryStorage<T> {
past: Vec<T>,
present: T,
future: Vec<T>,
}
impl<T: Clone> HistoryStorage<T> {
fn new(initial: T) -> Self {
Self {
past: Vec::new(),
present: initial,
future: Vec::new(),
}
}
fn set(&mut self, value: T) {
self.past.push(self.present.clone());
self.present = value;
self.future.clear();
}
fn undo(&mut self) -> bool {
if let Some(prev) = self.past.pop() {
self.future.push(self.present.clone());
self.present = prev;
true
} else {
false
}
}
fn redo(&mut self) -> bool {
if let Some(next) = self.future.pop() {
self.past.push(self.present.clone());
self.present = next;
true
} else {
false
}
}
}
#[derive(Clone)]
struct SharedHistoryStorage<T> {
inner: Arc<RwLock<HistoryStorage<T>>>,
}
impl<T: Clone> SharedHistoryStorage<T> {
fn new(initial: T) -> Self {
Self {
inner: Arc::new(RwLock::new(HistoryStorage::new(initial))),
}
}
}
#[derive(Clone)]
pub struct HistoryHandle<T> {
fiber_id: FiberId,
hook_index: usize,
storage: SharedHistoryStorage<T>,
}
impl<T: Clone + Send + Sync + 'static> HistoryHandle<T> {
pub fn current(&self) -> T {
self.storage
.inner
.read()
.expect("Failed to read history storage")
.present
.clone()
}
pub fn set(&self, value: T) {
{
let mut storage = self
.storage
.inner
.write()
.expect("Failed to write history storage");
storage.set(value);
}
self.mark_dirty();
}
pub fn undo(&self) -> bool {
let success = {
let mut storage = self
.storage
.inner
.write()
.expect("Failed to write history storage");
storage.undo()
};
if success {
self.mark_dirty();
}
success
}
pub fn redo(&self) -> bool {
let success = {
let mut storage = self
.storage
.inner
.write()
.expect("Failed to write history storage");
storage.redo()
};
if success {
self.mark_dirty();
}
success
}
pub fn can_undo(&self) -> bool {
!self
.storage
.inner
.read()
.expect("Failed to read history storage")
.past
.is_empty()
}
pub fn can_redo(&self) -> bool {
!self
.storage
.inner
.read()
.expect("Failed to read history storage")
.future
.is_empty()
}
pub fn past_count(&self) -> usize {
self.storage
.inner
.read()
.expect("Failed to read history storage")
.past
.len()
}
pub fn future_count(&self) -> usize {
self.storage
.inner
.read()
.expect("Failed to read history storage")
.future
.len()
}
pub fn clear_history(&self) {
let mut storage = self
.storage
.inner
.write()
.expect("Failed to write history storage");
storage.past.clear();
storage.future.clear();
}
pub fn go_back(&self, steps: usize) -> usize {
let mut taken = 0;
for _ in 0..steps {
if self.undo() {
taken += 1;
} else {
break;
}
}
taken
}
pub fn go_forward(&self, steps: usize) -> usize {
let mut taken = 0;
for _ in 0..steps {
if self.redo() {
taken += 1;
} else {
break;
}
}
taken
}
fn mark_dirty(&self) {
queue_update(
self.fiber_id,
StateUpdate {
hook_index: self.hook_index,
update: StateUpdateKind::Updater(Box::new(|any| {
let storage = any
.downcast_ref::<SharedHistoryStorage<T>>()
.expect("History storage type mismatch");
Box::new(storage.clone())
})),
},
);
}
}
pub fn use_history<T, F>(initializer: F) -> HistoryHandle<T>
where
T: Clone + Send + Sync + 'static,
F: FnOnce() -> T,
{
with_current_fiber(|fiber| {
let hook_index = fiber.next_hook_index();
let fiber_id = fiber.id;
let existing_storage: Option<SharedHistoryStorage<T>> = fiber.get_hook(hook_index);
let storage = if let Some(storage) = existing_storage {
storage
} else {
let storage = SharedHistoryStorage::new(initializer());
fiber.set_hook(hook_index, storage.clone());
storage
};
HistoryHandle {
fiber_id,
hook_index,
storage,
}
})
.expect("use_history must be called within a component render context")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fiber::FiberId;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree};
fn setup_test_fiber() -> FiberId {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.begin_render(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test() {
clear_fiber_tree();
crate::scheduler::batch::clear_state_batch();
}
#[test]
fn test_use_history_basic() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
assert_eq!(history.current(), 0);
cleanup_test();
}
#[test]
fn test_use_history_set() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
history.set(1);
assert_eq!(history.current(), 1);
history.set(2);
assert_eq!(history.current(), 2);
cleanup_test();
}
#[test]
fn test_use_history_undo() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
history.set(1);
history.set(2);
assert!(history.undo());
assert_eq!(history.current(), 1);
assert!(history.undo());
assert_eq!(history.current(), 0);
assert!(!history.undo());
assert_eq!(history.current(), 0);
cleanup_test();
}
#[test]
fn test_use_history_redo() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
history.set(1);
history.set(2);
history.undo();
history.undo();
assert!(history.redo());
assert_eq!(history.current(), 1);
assert!(history.redo());
assert_eq!(history.current(), 2);
assert!(!history.redo());
assert_eq!(history.current(), 2);
cleanup_test();
}
#[test]
fn test_use_history_set_clears_future() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
history.set(1);
history.set(2);
history.undo(); assert!(history.can_redo());
history.set(3); assert!(!history.can_redo()); assert_eq!(history.current(), 3);
cleanup_test();
}
#[test]
fn test_use_history_can_undo_redo() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
assert!(!history.can_undo());
assert!(!history.can_redo());
history.set(1);
assert!(history.can_undo());
assert!(!history.can_redo());
history.undo();
assert!(!history.can_undo());
assert!(history.can_redo());
cleanup_test();
}
#[test]
fn test_use_history_past_future_count() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
assert_eq!(history.past_count(), 0);
assert_eq!(history.future_count(), 0);
history.set(1);
history.set(2);
history.set(3);
assert_eq!(history.past_count(), 3);
assert_eq!(history.future_count(), 0);
history.undo();
history.undo();
assert_eq!(history.past_count(), 1);
assert_eq!(history.future_count(), 2);
cleanup_test();
}
#[test]
fn test_use_history_clear_history() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
history.set(1);
history.set(2);
history.undo();
history.clear_history();
assert!(!history.can_undo());
assert!(!history.can_redo());
assert_eq!(history.current(), 1);
cleanup_test();
}
#[test]
fn test_use_history_go_back_forward() {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0);
history.set(1);
history.set(2);
history.set(3);
history.set(4);
assert_eq!(history.go_back(2), 2);
assert_eq!(history.current(), 2);
assert_eq!(history.go_forward(3), 2); assert_eq!(history.current(), 4);
cleanup_test();
}
#[test]
fn test_use_history_stable_across_renders() {
let fiber_id = setup_test_fiber();
let history1 = use_history(|| 0);
history1.set(1);
history1.set(2);
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let history2 = use_history(|| 999);
assert_eq!(history2.current(), 2);
assert!(history2.can_undo());
cleanup_test();
}
#[test]
fn test_use_history_with_string() {
let _fiber_id = setup_test_fiber();
let history = use_history(String::new);
history.set("Hello".to_string());
history.set("Hello World".to_string());
assert_eq!(history.current(), "Hello World");
history.undo();
assert_eq!(history.current(), "Hello");
history.undo();
assert_eq!(history.current(), "");
cleanup_test();
}
#[test]
#[should_panic(expected = "use_history must be called within a component render context")]
fn test_use_history_panics_outside_render() {
clear_fiber_tree();
crate::scheduler::batch::clear_state_batch();
let _ = use_history(|| 0);
}
}
#[cfg(test)]
mod property_tests {
use super::*;
use crate::fiber::FiberId;
use crate::fiber_tree::{FiberTree, clear_fiber_tree, set_fiber_tree};
use proptest::prelude::*;
fn setup_test_fiber() -> FiberId {
let mut tree = FiberTree::new();
let fiber_id = tree.mount(None, None);
tree.begin_render(fiber_id);
set_fiber_tree(tree);
fiber_id
}
fn cleanup_test() {
clear_fiber_tree();
crate::scheduler::batch::clear_state_batch();
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_history_undo_redo_round_trip(v1 in any::<i32>(), v2 in any::<i32>()) {
let _fiber_id = setup_test_fiber();
let history = use_history(|| v1);
history.set(v2);
prop_assert_eq!(history.current(), v2);
prop_assert!(history.undo());
prop_assert_eq!(history.current(), v1);
prop_assert!(history.redo());
prop_assert_eq!(history.current(), v2);
cleanup_test();
}
#[test]
fn prop_history_multiple_undo_redo_round_trip(values in prop::collection::vec(any::<i32>(), 1..20)) {
let _fiber_id = setup_test_fiber();
let initial = values[0];
let history = use_history(|| initial);
for &v in &values[1..] {
history.set(v);
}
let mut undo_count = 0;
while history.undo() {
undo_count += 1;
}
prop_assert_eq!(undo_count, values.len() - 1);
prop_assert_eq!(history.current(), initial);
let mut redo_count = 0;
while history.redo() {
redo_count += 1;
}
prop_assert_eq!(redo_count, values.len() - 1);
prop_assert_eq!(history.current(), *values.last().unwrap());
cleanup_test();
}
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn prop_history_tracks_all_values(values in prop::collection::vec(any::<i32>(), 2..20)) {
let _fiber_id = setup_test_fiber();
let initial = values[0];
let history = use_history(|| initial);
prop_assert!(!history.can_undo());
for (i, &v) in values[1..].iter().enumerate() {
history.set(v);
prop_assert!(history.can_undo(), "Should be able to undo after set #{}", i + 1);
prop_assert_eq!(history.past_count(), i + 1, "Past count should be {} after set #{}", i + 1, i + 1);
}
for expected in values.iter().rev().skip(1) {
prop_assert!(history.undo());
prop_assert_eq!(history.current(), *expected);
}
cleanup_test();
}
#[test]
fn prop_history_set_clears_future(
initial in any::<i32>(),
v1 in any::<i32>(),
v2 in any::<i32>(),
v3 in any::<i32>()
) {
let _fiber_id = setup_test_fiber();
let history = use_history(|| initial);
history.set(v1);
history.set(v2);
history.undo();
prop_assert!(history.can_redo());
prop_assert_eq!(history.future_count(), 1);
history.set(v3);
prop_assert!(!history.can_redo());
prop_assert_eq!(history.future_count(), 0);
cleanup_test();
}
#[test]
fn prop_history_can_undo_redo_consistency(ops in prop::collection::vec(prop_oneof![Just(true), Just(false)], 1..30)) {
let _fiber_id = setup_test_fiber();
let history = use_history(|| 0i32);
for i in 1..=5 {
history.set(i);
}
for op in ops {
if op {
let could_undo = history.can_undo();
let did_undo = history.undo();
prop_assert_eq!(could_undo, did_undo, "can_undo should predict undo success");
} else {
let could_redo = history.can_redo();
let did_redo = history.redo();
prop_assert_eq!(could_redo, did_redo, "can_redo should predict redo success");
}
}
cleanup_test();
}
#[test]
fn prop_history_past_future_count_invariant(values in prop::collection::vec(any::<i32>(), 1..15)) {
let _fiber_id = setup_test_fiber();
let initial = values[0];
let history = use_history(|| initial);
for &v in &values[1..] {
history.set(v);
}
let total_history = values.len() - 1;
for _ in 0..total_history {
let past = history.past_count();
let future = history.future_count();
prop_assert_eq!(
past + future, total_history,
"past + future should equal total history"
);
history.undo();
}
cleanup_test();
}
#[test]
fn prop_history_stable_across_renders(initial in any::<i32>(), values in prop::collection::vec(any::<i32>(), 1..10)) {
let fiber_id = setup_test_fiber();
let history1 = use_history(|| initial);
for &v in &values {
history1.set(v);
}
let expected_current = *values.last().unwrap();
let expected_past_count = values.len();
crate::fiber_tree::with_fiber_tree_mut(|tree| {
tree.end_render();
tree.begin_render(fiber_id);
});
let history2 = use_history(|| 999);
prop_assert_eq!(history2.current(), expected_current);
prop_assert_eq!(history2.past_count(), expected_past_count);
cleanup_test();
}
}
}