Skip to content

Commit

Permalink
UndoManager: add different types of undo/redo ops
Browse files Browse the repository at this point in the history
  • Loading branch information
Horusiath committed Aug 27, 2024
1 parent fafa671 commit b942b2f
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 51 deletions.
86 changes: 81 additions & 5 deletions tests-ffi/include/libyrs.h
Original file line number Diff line number Diff line change
Expand Up @@ -2504,35 +2504,111 @@ struct YEventKeyChange *yxmltext_event_keys(const struct YXmlTextEvent *e, uint3
*/
void yevent_keys_destroy(struct YEventKeyChange *keys, uint32_t len);

/**
* Creates a new instance of undo manager bound to a current `doc`. It can be used to track
* specific shared refs via `yundo_manager_add_scope` and updates coming from specific origin
* - like ability to undo/redo operations originating only at the local peer - by using
* `yundo_manager_add_origin`.
*
* This object can be deallocated via `yundo_manager_destroy`.
*/
YUndoManager *yundo_manager(const YDoc *doc, const struct YUndoManagerOptions *options);

/**
* Deallocated undo manager instance created via `yundo_manager`.
*/
void yundo_manager_destroy(YUndoManager *mgr);

/**
* Adds an origin to be tracked by current undo manager. This way only changes made within context
* of transactions created with specific origin will be subjects of undo/redo operations. This is
* useful when you want to be able to revert changed done by specific user without reverting
* changes made by other users that were applied in the meantime.
*/
void yundo_manager_add_origin(YUndoManager *mgr, uint32_t origin_len, const char *origin);

/**
* Removes an origin previously added to undo manager via `yundo_manager_add_origin`.
*/
void yundo_manager_remove_origin(YUndoManager *mgr, uint32_t origin_len, const char *origin);

/**
* Add specific shared type to be tracked by this instance of an undo manager.
*/
void yundo_manager_add_scope(YUndoManager *mgr, const Branch *ytype);

uint8_t yundo_manager_clear(YUndoManager *mgr);
/**
* Removes all the undo/redo stack changes tracked by current undo manager. This also cleans up
* all the items that couldn't be deallocated / garbage collected for the sake of possible
* undo/redo operations.
*
* Keep in mind that this function call requires that underlying document store is not concurrently
* modified by other read-write transaction. This is done by acquiring the read-only transaction
* itself. If such transaction could be acquired (because of another read-write transaction is in
* progress, this function will hold current thread until acquisition is possible.
*/
void yundo_manager_clear(YUndoManager *mgr);

/**
* Cuts off tracked changes, producing a new stack item on undo stack.
*
* By default, undo manager gathers undergoing changes together into undo stack items on periodic
* basis (defined by `YUndoManagerOptions.capture_timeout_millis`). By calling this function, we're
* explicitly creating a new stack item will all the changes registered since last stack item was
* created.
*/
void yundo_manager_stop(YUndoManager *mgr);

/**
* Performs an undo operations, reverting all the changes defined by the last undo stack item.
* These changes can be then reapplied again by calling `yundo_manager_redo` function.
*
* Returns `Y_TRUE` if successfully managed to do an undo operation.
* Returns `Y_FALSE` if undo stack was empty or if undo couldn't be performed (because another
* transaction is in progress).
*/
uint8_t yundo_manager_undo(YUndoManager *mgr);

/**
* Performs a redo operations, reapplying changes undone by `yundo_manager_undo` operation.
*
* Returns `Y_TRUE` if successfully managed to do a redo operation.
* Returns `Y_FALSE` if redo stack was empty or if redo couldn't be performed (because another
* transaction is in progress).
*/
uint8_t yundo_manager_redo(YUndoManager *mgr);

uint8_t yundo_manager_can_undo(YUndoManager *mgr);
/**
* Returns number of elements stored on undo stack.
*/
uint32_t yundo_manager_undo_stack_len(YUndoManager *mgr);

uint8_t yundo_manager_can_redo(YUndoManager *mgr);
/**
* Returns number of elements stored on redo stack.
*/
uint32_t yundo_manager_redo_stack_len(YUndoManager *mgr);

/**
* Subscribes a `callback` function pointer to a given undo manager event. This event will be
* triggered every time a new undo/redo stack item is added.
*
* Returns a subscription pointer that can be used to cancel current callback registration via
* `yunobserve`.
*/
YSubscription *yundo_manager_observe_added(YUndoManager *mgr,
void *state,
void (*cb)(void*, const struct YUndoEvent*));
void (*callback)(void*, const struct YUndoEvent*));

/**
* Subscribes a `callback` function pointer to a given undo manager event. This event will be
* triggered every time a undo/redo operation was called.
*
* Returns a subscription pointer that can be used to cancel current callback registration via
* `yunobserve`.
*/
YSubscription *yundo_manager_observe_popped(YUndoManager *mgr,
void *state,
void (*cb)(void*, const struct YUndoEvent*));
void (*callback)(void*, const struct YUndoEvent*));

/**
* Returns a value informing what kind of Yrs shared collection given `branch` represents.
Expand Down
12 changes: 4 additions & 8 deletions tests-wasm/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 62 additions & 23 deletions yffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4567,6 +4567,12 @@ pub struct YUndoManagerOptions {
}

// TODO [LSViana] Maybe rename this to `yundo_manager_new_with_options` to match `ydoc_new_with_options`?
/// Creates a new instance of undo manager bound to a current `doc`. It can be used to track
/// specific shared refs via `yundo_manager_add_scope` and updates coming from specific origin
/// - like ability to undo/redo operations originating only at the local peer - by using
/// `yundo_manager_add_origin`.
///
/// This object can be deallocated via `yundo_manager_destroy`.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager(
doc: *const Doc,
Expand All @@ -4584,11 +4590,16 @@ pub unsafe extern "C" fn yundo_manager(
Box::into_raw(boxed)
}

/// Deallocated undo manager instance created via `yundo_manager`.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_destroy(mgr: *mut YUndoManager) {
drop(Box::from_raw(mgr));
}

/// Adds an origin to be tracked by current undo manager. This way only changes made within context
/// of transactions created with specific origin will be subjects of undo/redo operations. This is
/// useful when you want to be able to revert changed done by specific user without reverting
/// changes made by other users that were applied in the meantime.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_add_origin(
mgr: *mut YUndoManager,
Expand All @@ -4600,6 +4611,7 @@ pub unsafe extern "C" fn yundo_manager_add_origin(
mgr.include_origin(Origin::from(bytes));
}

/// Removes an origin previously added to undo manager via `yundo_manager_add_origin`.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_remove_origin(
mgr: *mut YUndoManager,
Expand All @@ -4611,101 +4623,128 @@ pub unsafe extern "C" fn yundo_manager_remove_origin(
mgr.exclude_origin(Origin::from(bytes));
}

/// Add specific shared type to be tracked by this instance of an undo manager.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_add_scope(mgr: *mut YUndoManager, ytype: *const Branch) {
let mgr = mgr.as_mut().unwrap();
let branch = ytype.as_ref().unwrap();
mgr.expand_scope(&BranchPtr::from(branch));
}

/// Removes all the undo/redo stack changes tracked by current undo manager. This also cleans up
/// all the items that couldn't be deallocated / garbage collected for the sake of possible
/// undo/redo operations.
///
/// Keep in mind that this function call requires that underlying document store is not concurrently
/// modified by other read-write transaction. This is done by acquiring the read-only transaction
/// itself. If such transaction could be acquired (because of another read-write transaction is in
/// progress, this function will hold current thread until acquisition is possible.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_clear(mgr: *mut YUndoManager) -> u8 {
pub unsafe extern "C" fn yundo_manager_clear(mgr: *mut YUndoManager) {
let mgr = mgr.as_mut().unwrap();
match mgr.clear() {
Ok(_) => Y_TRUE,
Err(_) => Y_FALSE,
}
mgr.clear();
}

/// Cuts off tracked changes, producing a new stack item on undo stack.
///
/// By default, undo manager gathers undergoing changes together into undo stack items on periodic
/// basis (defined by `YUndoManagerOptions.capture_timeout_millis`). By calling this function, we're
/// explicitly creating a new stack item will all the changes registered since last stack item was
/// created.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_stop(mgr: *mut YUndoManager) {
let mgr = mgr.as_mut().unwrap();
mgr.reset();
}

/// Performs an undo operations, reverting all the changes defined by the last undo stack item.
/// These changes can be then reapplied again by calling `yundo_manager_redo` function.
///
/// Returns `Y_TRUE` if successfully managed to do an undo operation.
/// Returns `Y_FALSE` if undo stack was empty or if undo couldn't be performed (because another
/// transaction is in progress).
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_undo(mgr: *mut YUndoManager) -> u8 {
let mgr = mgr.as_mut().unwrap();

match mgr.undo() {
match mgr.try_undo() {
Ok(true) => Y_TRUE,
Ok(false) => Y_FALSE,
Err(_) => Y_FALSE,
}
}

/// Performs a redo operations, reapplying changes undone by `yundo_manager_undo` operation.
///
/// Returns `Y_TRUE` if successfully managed to do a redo operation.
/// Returns `Y_FALSE` if redo stack was empty or if redo couldn't be performed (because another
/// transaction is in progress).
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_redo(mgr: *mut YUndoManager) -> u8 {
let mgr = mgr.as_mut().unwrap();
match mgr.redo() {
match mgr.try_redo() {
Ok(true) => Y_TRUE,
Ok(false) => Y_FALSE,
Err(_) => Y_FALSE,
}
}

/// Returns number of elements stored on undo stack.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_can_undo(mgr: *mut YUndoManager) -> u8 {
pub unsafe extern "C" fn yundo_manager_undo_stack_len(mgr: *mut YUndoManager) -> u32 {
let mgr = mgr.as_mut().unwrap();
if mgr.can_undo() {
Y_TRUE
} else {
Y_FALSE
}
mgr.undo_stack().len() as u32
}

/// Returns number of elements stored on redo stack.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_can_redo(mgr: *mut YUndoManager) -> u8 {
pub unsafe extern "C" fn yundo_manager_redo_stack_len(mgr: *mut YUndoManager) -> u32 {
let mgr = mgr.as_mut().unwrap();
if mgr.can_redo() {
Y_TRUE
} else {
Y_FALSE
}
mgr.redo_stack().len() as u32
}

/// Subscribes a `callback` function pointer to a given undo manager event. This event will be
/// triggered every time a new undo/redo stack item is added.
///
/// Returns a subscription pointer that can be used to cancel current callback registration via
/// `yunobserve`.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_observe_added(
mgr: *mut YUndoManager,
state: *mut c_void,
cb: extern "C" fn(*mut c_void, *const YUndoEvent),
callback: extern "C" fn(*mut c_void, *const YUndoEvent),
) -> *mut Subscription {
let state = CallbackState::new(state);
let mgr = mgr.as_mut().unwrap();
let subscription = mgr.observe_item_added(move |_, e| {
let meta_ptr = {
let event = YUndoEvent::new(e);
cb(state.0, &event as *const YUndoEvent);
callback(state.0, &event as *const YUndoEvent);
event.meta
};
e.meta().store(meta_ptr, Ordering::Release);
});
Box::into_raw(Box::new(subscription))
}

/// Subscribes a `callback` function pointer to a given undo manager event. This event will be
/// triggered every time a undo/redo operation was called.
///
/// Returns a subscription pointer that can be used to cancel current callback registration via
/// `yunobserve`.
#[no_mangle]
pub unsafe extern "C" fn yundo_manager_observe_popped(
mgr: *mut YUndoManager,
state: *mut c_void,
cb: extern "C" fn(*mut c_void, *const YUndoEvent),
callback: extern "C" fn(*mut c_void, *const YUndoEvent),
) -> *mut Subscription {
let mgr = mgr.as_mut().unwrap();
let state = CallbackState::new(state);
let subscription = mgr
.observe_item_popped(move |_, e| {
let meta_ptr = {
let event = YUndoEvent::new(e);
cb(state.0, &event as *const YUndoEvent);
callback(state.0, &event as *const YUndoEvent);
event.meta
};
e.meta().store(meta_ptr, Ordering::Release);
Expand Down
4 changes: 2 additions & 2 deletions yrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,11 +331,11 @@
//! assert_eq!(text1.get_string(&local.transact()), "hello worldeveryone"); // remote changes synced
//!
//! // undo last performed change on local
//! mgr.undo().unwrap();
//! mgr.undo_blocking();
//! assert_eq!(text1.get_string(&local.transact()), "hello everyone");
//!
//! // redo change we undone
//! mgr.redo().unwrap();
//! mgr.redo_blocking();
//! assert_eq!(text1.get_string(&local.transact()), "hello worldeveryone");
//! ```
//!
Expand Down
19 changes: 14 additions & 5 deletions yrs/src/undo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::iter::TxnIterator;
use crate::slice::BlockSlice;
use crate::sync::Clock;
use crate::transaction::Origin;
use crate::{DeleteSet, Doc, Observer, Transact, TransactionAcqError, TransactionMut, ID};
use crate::{DeleteSet, Doc, Observer, ReadTxn, Transact, TransactionAcqError, TransactionMut, ID};

/// Undo manager is a structure used to perform undo/redo operations over the associated shared
/// type(s).
Expand Down Expand Up @@ -482,21 +482,30 @@ where
}

/// Clears all [StackItem]s stored within current UndoManager, effectively resetting its state.
pub fn clear(&mut self, txn: &mut TransactionMut) {
///
/// # Deadlocks
///
/// In order to perform its function, this method must guarantee that underlying document store
/// is not being modified by another running `TransactionMut`. It does so by acquiring
/// a read-only transaction itself. If transaction couldn't be acquired (because another
/// read-write transaction is in progress), it will hold current thread until, acquisition is
/// available.
pub fn clear(&mut self) {
let txn = self.doc.transact();
let inner = Arc::get_mut(&mut self.state).unwrap();

let len = inner.undo_stack.len();
for item in inner.undo_stack.drain(0..len) {
Self::clear_item(&inner.scope, txn, item);
Self::clear_item(&inner.scope, &txn, item);
}

let len = inner.redo_stack.len();
for item in inner.redo_stack.drain(0..len) {
Self::clear_item(&inner.scope, txn, item);
Self::clear_item(&inner.scope, &txn, item);
}
}

fn clear_item(scope: &HashSet<BranchPtr>, txn: &mut TransactionMut, stack_item: StackItem<M>) {
fn clear_item<T: ReadTxn>(scope: &HashSet<BranchPtr>, txn: &T, stack_item: StackItem<M>) {
let mut deleted = stack_item.deletions.deleted_blocks();
while let Some(slice) = deleted.next(txn) {
if let Some(item) = slice.as_item() {
Expand Down
Loading

0 comments on commit b942b2f

Please sign in to comment.