Skip to main content

Crate undoredo

Crate undoredo 

Source
Expand description

Docs Crates.io MIT OR Apache 2.0

§undoredo

undoredo is a Rust library that implements the Undo/Redo pattern on arbitrary data structures by automatically recording sparse deltas (aka. diffs, patches) of changes, or whole snapshots of past states (Memento pattern).

This approach is much easier than the commonly used Command pattern, which is the principle of operation of other Undo/Redo crates, as having to implement commands requires maintenance of additional application logic that is often complicated and can lead to elusive bugs. But if needed, undoredo can also store a command or other metadata along with every edit, allowing for easy use of the Command pattern as well.

Delta-recording undo-redo requires creating a separate delta edit type for each data structure. For ease of use, undoredo has derive macro #[derive(Delta)] to automatically generate these types on arbitrary custom structs and enums, as well as convenience implementations for standard library collections: HashMap, HashSet, BTreeMap, BTreeSet, Vec, and for some third-party feature-gated types: StableVec, thunderdome::Arena, rstar::RTree, rstared::RTreed (read more in the Supported containers section).

This library is compatible with no_std and serde and has no mandatory third-party dependencies except for alloc.

§Demo

Animation showing polygons being added and subtracted in the demo stored in demos/polygon_set/ directory of the repository of the polygon_unionfind crate.

The above demo animation shows Undo/Redo action over dynamically added and subtracted polygons with R-tree spatial indexing. Neither commands nor snapshots are stored, but all edits in history are made of sparse deltas of the polygon container and the associated R-tree:

Source code with instructions for running the above interactive visualization can be found in the repository of the polygon_unionfind crate (see its README for details).

§Usage

§Adding dependency

First, add undoredo as a dependency to your Cargo.toml:

[dependencies]
undoredo = { version = "0.10.13", features = ["derive"] }

The derive feature flag is only required when using deltas on custom struct or enum types to derive delta edit types. Snapshots and commands work without any derives.

§Usage examples

§Delta recorder over BTreeMap

Following is a basic usage example of delta-recording undo-redo over BTreeMap. You can find more examples in the examples/ directory.

use std::collections::BTreeMap;
use undoredo::{BTreeMapDelta, Recorder, UndoRedo};

fn main() {
    // The recorder records the ongoing changes to the recorded container.
    let mut recorder: Recorder<BTreeMap<usize, char>> =
        Recorder::new(BTreeMap::new());

    // The undo-redo struct that holds and maintains the undo-redo bistack.
    let mut undoredo: UndoRedo<BTreeMapDelta<usize, char>> = UndoRedo::new();

    // Push elements while recording the changes in a delta.
    recorder.insert(1, 'A');
    recorder.insert(2, 'B');
    recorder.insert(3, 'C');

    // The pushed elements are now present in the container.
    assert!(*recorder.container() == BTreeMap::from([(1, 'A'), (2, 'B'), (3, 'C')]));

    // Flush the recorder and commit the recorded delta of pushing 'A', 'B', 'C'
    // into the undo-redo bistack.
    undoredo.commit(&mut recorder);

    // Now undo the action.
    undoredo.undo(&mut recorder);

    // The container is now empty; the action of pushing elements has been undone.
    assert!(*recorder.container() == BTreeMap::from([]));

    // Now redo the action.
    undoredo.redo(&mut recorder);

    // The elements are back in the container; the action has been redone.
    assert!(*recorder.container() == BTreeMap::from([(1, 'A'), (2, 'B'), (3, 'C')]));

    // Once you are done recording, you can dissolve the recorder to regain
    // ownership and mutability over the recorded container.
    let (btreemap, ..) = recorder.dissolve();
    assert!(btreemap == BTreeMap::from([(1, 'A'), (2, 'B'), (3, 'C')]));
}
§Delta recorder over custom struct or enum

A data structure may not be one of the supported containers, but it may be a custom struct or enum, made of multiple containers or other members, possibly with additional generics and fields that should not be subject to undo-redo. To be able to perform delta recording in such case, you need to implement a delta structure for it together with two operations, .apply_delta() and .flush_delta(). This can usually be easily done automatically by just applying a provided derive macro, #[derive(Delta)] .

Below is an example of an undoredoable struct-of-arrays storage with generic coordinate type K that could be used for a simple entity-component-system:

// No need for `#[derive(Delta)]` for types stored in containers, only the
// containers themselves need this.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Vector2<T> {
    x: T,
    y: T,
}

// `#[derive(Delta)]` generates the `EntitiesDelta` type, which is needed to
// store deltas as edits in an `UndoRedo` bistack.
#[derive(Delta)]
// You can choose the name for the generated delta edit type through the
// `undoredo` attribute with key `delta`. If you omit this, the default is
// the name of the input type followed by the word `Delta`. The name chosen in
// this example, `EntitiesDelta`, happens to be the same as the default.
#[undoredo(delta = EntitiesDelta)]
// Each delta is made of two collections of elements called "half-deltas".
// `#[derive(Delta)]` generates a type for them as well, named similarly to full
// deltas. If that name does not suit you, you can analogously use the
// `undoredo` attribute with key `half_delta` to rename them.,
#[undoredo(half_delta = EntitiesHalfDelta)]
pub struct Entities<T> {
    positions: Recorder<Vec<Vector2<T>>>,
    velocities: Recorder<Vec<Vector2<T>>>,
    healths: Recorder<Vec<i64>>,
    // You can record changes to primitive types too, not just collections.
    turn_counter: Recorder<u64>,
    // You can make fields not be subject to undo-redo by marking them with
    // `#[undoredo(skip)]`.
    // Note that this skipping only works for delta-based undo-redo because
    // snapshots and commands rely on different mechanisms.
    #[undoredo(skip)]
    not_in_delta: String,
}

See examples/entities.rs for the full example.

§Storing command metadata along with edits

It is often desirable to store some metadata along with every recorded edit, usually some representation of the command that originated it. This can be done by instead committing the edit using the .cmd_commit() method.

The bistack of done and undone committed edits, together with their command metadatas (“cmd”) if present, can be accessed as slices from the .done() and .undone() accessor methods.

use std::collections::BTreeMap;
use undoredo::{BTreeMapDelta, Recorder, UndoRedo};

// Representation of the command that originated the recorded delta.
#[derive(Debug, Clone, PartialEq)]
enum Command {
    PushChar,
}

fn main() {
    let mut recorder: Recorder<BTreeMap<usize, char>> =
        Recorder::new(BTreeMap::new());
    let mut undoredo: UndoRedo<BTreeMapDelta<usize, char>, Command> = UndoRedo::new();

    recorder.insert(1, 'A');
    recorder.insert(2, 'B');

    // Commit `Command::PushChar` enum variant as command metadata ("cmd") along
    // with the recorded delta.
    undoredo.cmd_commit(Command::PushChar, recorder.flush_delta());

    // `Command::PushChar` is now the top element of the stack of done cmd-deltas.
    assert_eq!(undoredo.done().last().unwrap().cmd, Command::PushChar);

    undoredo.undo(&mut recorder);

    // After undo, `Command::PushChar` is now the top element of the stack of
    // undone cmd-deltas.
    assert_eq!(undoredo.undone().last().unwrap().cmd, Command::PushChar);
}
§Command pattern

You can also give up edits altogether and only store commands in the metadata field, thereby implementing the Command pattern. See examples/commands.rs for an example.

§Snapshot-based undo-redo

You can easily perform undo-redo using snapshots (that is, by storing whole past states instead of only diffs between them) instead of deltas or commands. If you were using deltas, you need to remove the Recorder<...> wrapper over your container and instead use the Snapshot<...> wrapper in place of the Delta<...> type.

For instance, if your container is HashMap<usize, char>, it will look like this:

let mut container: HashMap<usize, char> = HashMap::new();
let mut undoredo: UndoRedo<Snapshot<HashMap<usize, char>>> = UndoRedo::new();

This will work for all container types that implement Clone, without need for any derive macro invocations or additional trait implementations as is with deltas.

However, there are two caveats not present with deltas: if you want to be able to restore the container to its initial (often empty) state, you need to commit it first. And if you end up committing your latest changes, to revert them you will need to invoke undo twice, since the first undo just restores the state to the top of the undone stack.

For a full usage example, see examples/snapshots.rs.

§Delta recording on maps with pushing

Some containers with map semantics also provide a special type of insertion where a value is inserted without specifying a key, which the container instead automatically generates and returns by itself. In undoredo, this operation is called pushing.

If a supported type has such a push interface, you can record its changes just as easily as insertions and removals by calling .push() on the recorder, like this:

recorder.push('A');

StableVec and thunderdome::Arena are instances of supported pushable maps. See examples/stable_vec.rs and examples/thunderdome.rs for complete examples of their usage.

§Delta recording on sets

Some containers have set semantics: they operate only on values, without exposing any usable notion of key or index. undoredo can provide delta-based undo-redo functionality to such a set by treating it as a ()-valued map whose keys are the set’s values. This is actually also how Rust’s standard library internally represents its two set types, HashSet and BTreeSet.

As an example, the following code will construct a recorder and an undo-redo bistack for a BTreeSet:

let mut recorder: Recorder<BTreeSet<char>> = Recorder::new(BTreeSet::new());
let mut undoredo: UndoRedo<Delta<BTreeSet<char>>> = UndoRedo::new();

Keeping in mind to pass values as keys, recorder and undoredo can then be used the same way as with maps above. See examples/btreeset.rs for a complete example.

Among the supported third-party types, rstar::RTree is one data structure for which undoredo has a convenience implementation over set semantics. See examples/rstar.rs for an example of its usage.

NOTE: Some set-like data structures are actually multisets: they allow to insert the same value multiple times without overriding the first one. In fact, rstar::RTree is a multiset. undoredo will work correctly with such data structures, seeing them as sets, but only if you never make use of their multiset property: you must never insert a key again when it is already present in a multiset.

§Supported containers

§When using snapshots

All data structures that implement the Clone trait are supported for snapshot-based undo-redo.

§When using commands

For command-based undo-redo, obviously all data structures are supported as long as the library user implements commands for them.

§When using deltas

To be able to use delta-based undo-redo on a data structure, it is necessary to have a delta structure as well as two operations, .apply_delta() and .flush_delta(), available as traits. For ease of use, undoredo supplies a number of built-in convenience implementations of all these for various commonly-used container types.

For custom data structures, there is a derive macro, #[derive(Delta)], that will generate all the necessary code.

Though we try our best, this derive macro may not work on some highly-generic data types. And in some cases, it may be desirable to add some additional logic to the operations. In these situations, it is necessary to implement the delta structure and operations on it manually.

For an example of a manual implementation, take a look at mikwielgus/polygon_unionfind/src/unionfind.rs. This is a delta-undoredoable union-find where every field’s type is a generic parameter.

§Standard library

Rust’s standard library maps and sets have built-in convenience implementations of delta-editing:

  • HashMap, gated by the std feature flag (enabled by default);
  • HashSet, gated by the std feature flag (enabled by default);
  • BTreeMap, not feature-gated;
  • BTreeSet, not feature-gated;
  • Vec, not feature-gated.
§Third-party types

In addition to the standard library, undoredo has also has built-in feature-gated convenience implementations of delta-editing for data structures from certain external crates:

To use these, enable their corresponding feature flags next to your undoredo dependency in your Cargo.toml. For example, to enable all third-party type implementations, write

[dependencies]
undoredo = { version = "0.10.13", features = ["stable-vec", "thunderdome", "rstar", "rstared"] }
§Custom types

To make delta-based undo-redo work with a map-like non-composite container data structure for which there is no convenience implementation, you can create one on your own by implementing the traits from the maplike crate. Refer to that crate’s documentation for details. These traits are also re-exported by undoredo, so it is not necessary to add another dependency.

If you believe that other people could benefit from your implementation, consider contributing it to maplike. We will integrate it in undoredo on our own afterwards (no need to open more than one pull request).

§Unsupported containers

Some containers cannot be supported for delta-based undo-redo because they lack an interface on which maplike’s traits could be implemented. See the Unsupported containers section in maplike’s documentation for details.

§Documentation

See the documentation for more information on undoredo’s usage.

§Packaging

undoredo is published as a crate on the Crates.io registry.

§Contributing

We welcome issues, pull requests and any other contributions from anyone to our repository on GitHub.

§Licence

§Outbound licence

undoredo is dual-licensed as under either of

at your option.

§Inbound licence

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work by you will be dual-licensed as described above, without any additional terms or conditions.

§Feature flags

  • derive — Enables the Delta derive macro (and also its constituent macros, ApplyDelta, FlushDelta, HalfDelta).
  • serde — Enables serde support (derives Serialize and Deserialize traits).
  • std (enabled by default) — Disable for no_std compatibility.
  • rstar — Enables support of delta-recording for rstar::RTree type.
  • rstared — Enables support of delta-recording for rstared::RTreed type.
  • stable-vec — Enables support of delta-recording for stable_vec::StableVec type.
  • thunderdome — Enables support of delta-recording for thunderdome::Arena type.

Re-exports§

pub extern crate alloc;

Structs§

CmdEdit
An edit along with metadata.
Delta
A reversible set of changes to a container.
Recorder
Records deltas applied to a container so that they can be replayed or reverted.
Snapshot
A snapshot of the whole state of a container.
UndoRedo
An undo-redo bistack.

Traits§

ApplyDelta
Apply the changes in a delta to a container.
Container
Base trait for keyed collections, without any operations defined yet.
Extract
Extract an edit corresponding to the current state.
FlushDelta
Flush the recorder, returning the recorded delta and replacing it with a new empty one.
Get
Returns a reference to the value corresponding to the key.
Insert
Insert a new key-value pair into the collection at an arbitrary key.
IntoIter
Consume the collection and yield owned key-value pairs.
Maplike
A keyed collection with get, set, insert, remove, clear operations.
Push
Insert a value into the collection without specifying a key, returning the key that was automatically generated.
Remove
Remove an element under a key from the collection, returning the value at the key if the key was previously in the map. Other keys are not invalidated.
Revert
Revert an edit and return an edit that reverts the revert.
Scalarlike
A single assignable value.
Setlike
A map-like keyed collection whose value is the unit type, thus behaving like a set.
Veclike
An array-like keyed collection with additional push, pop, clear operations.

Type Aliases§

BTreeMapDelta
Delta for BTreeMap<K, V>. Alias for Delta<BTreeMapHalfDelta<K, V>>.
BTreeMapHalfDelta
Half-delta for BTreeMap<K, V>. Alias for BTreeMap<K, V>.
BTreeSetDelta
Delta for BTreeSet<K>. Alias for Delta<BTreeSetHalfDelta<K>>.
BTreeSetHalfDelta
Half-delta for BTreeSet<K>. Alias for BTreeMap<K, ()>.
BoolDelta
Delta for bool. Alias for Delta<BoolHalfDelta>.
BoolHalfDelta
Half-delta for bool. Alias for BTreeMap<usize, bool>.
CharDelta
Delta for char. Alias for Delta<CharHalfDelta>.
CharHalfDelta
Half-delta for char. Alias for BTreeMap<usize, char>.
F32Delta
Delta for f32. Alias for Delta<F32HalfDelta>.
F32HalfDelta
Half-delta for f32. Alias for BTreeMap<usize, f32>.
F64Delta
Delta for f64. Alias for Delta<F64HalfDelta>.
F64HalfDelta
Half-delta for f64. Alias for BTreeMap<usize, f64>.
HashMapDeltastd
Delta for HashMap<K, V>. Alias for Delta<HashMapHalfDelta<K, V>>.
HashMapHalfDeltastd
Half-delta for HashMap<K, V>. Alias for HashMap<K, V>.
HashSetDeltastd
Delta for HashSet<K>. Alias for Delta<HashSetHalfDelta<K>>.
HashSetHalfDeltastd
Half-delta for HashSet<K>. Alias for HashMap<K, ()>.
I8Delta
Delta for i8. Alias for Delta<I8HalfDelta>.
I8HalfDelta
Half-delta for i8. Alias for BTreeMap<usize, i8>.
I16Delta
Delta for i16. Alias for Delta<I16HalfDelta>.
I16HalfDelta
Half-delta for i16. Alias for BTreeMap<usize, i16>.
I32Delta
Delta for i32. Alias for Delta<I32HalfDelta>.
I32HalfDelta
Half-delta for i32. Alias for BTreeMap<usize, i32>.
I64Delta
Delta for i64. Alias for Delta<I64HalfDelta>.
I64HalfDelta
Half-delta for i64. Alias for BTreeMap<usize, i64>.
I128Delta
Delta for i128. Alias for Delta<I128HalfDelta>.
I128HalfDelta
Half-delta for i128. Alias for BTreeMap<usize, i128>.
IsizeDelta
Delta for isize. Alias for Delta<IsizeHalfDelta>.
IsizeHalfDelta
Half-delta for isize. Alias for BTreeMap<usize, isize>.
PhantomDataDelta
Delta for PhantomData<V>. Alias for Delta<PhantomDataHalfDelta>.
PhantomDataHalfDelta
Half-delta for PhantomData<V>. Alias for BTreeMap<usize, ()>.
RTreeDeltarstar
Delta for RTree<K>. Alias for Delta<RTreeHalfDelta<K>>.
RTreeHalfDeltarstar
Half-delta for RTree<K>. Alias for BTreeMap<K, ()>.
RTreedDeltarstared
Delta for RTreed<C>. Alias for Delta<RTreedHalfDelta<K, V>>.
RTreedHalfDeltarstared
Half-delta for RTreed<C>. Alias for BTreeMap<K, V>.
StableVecDeltastable-vec
Delta for StableVecFacade<V, C>. Alias for Delta<StableVecHalfDelta<V>>.
StableVecHalfDeltastable-vec
Half-delta for StableVecFacade<V, C>. Alias for BTreeMap<usize, V>.
ThunderdomeDeltathunderdome
Delta for Arena<V>. Alias for Delta<ThunderdomeHalfDelta<V>>.
ThunderdomeHalfDeltathunderdome
Half-delta for Arena<V>. Alias for BTreeMap<Index, V>.
Tuple1Delta
Delta for (T0, ). Alias for Delta<Tuple1HalfDelta<T0, >>.
Tuple1HalfDelta
Half-delta for (T0, ). Alias for BTreeMap<usize, (T0, )>.
Tuple2Delta
Delta for (T0, T1, ). Alias for Delta<Tuple2HalfDelta<T0, T1, >>.
Tuple2HalfDelta
Half-delta for (T0, T1, ). Alias for BTreeMap<usize, (T0, T1, )>.
Tuple3Delta
Delta for (T0, T1, T2, ). Alias for Delta<Tuple3HalfDelta<T0, T1, T2, >>.
Tuple3HalfDelta
Half-delta for (T0, T1, T2, ). Alias for BTreeMap<usize, (T0, T1, T2, )>.
Tuple4Delta
Delta for (T0, T1, T2, T3, ). Alias for Delta<Tuple4HalfDelta<T0, T1, T2, T3, >>.
Tuple4HalfDelta
Half-delta for (T0, T1, T2, T3, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, )>.
Tuple5Delta
Delta for (T0, T1, T2, T3, T4, ). Alias for Delta<Tuple5HalfDelta<T0, T1, T2, T3, T4, >>.
Tuple5HalfDelta
Half-delta for (T0, T1, T2, T3, T4, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, T4, )>.
Tuple6Delta
Delta for (T0, T1, T2, T3, T4, T5, ). Alias for Delta<Tuple6HalfDelta<T0, T1, T2, T3, T4, T5, >>.
Tuple6HalfDelta
Half-delta for (T0, T1, T2, T3, T4, T5, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, T4, T5, )>.
Tuple7Delta
Delta for (T0, T1, T2, T3, T4, T5, T6, ). Alias for Delta<Tuple7HalfDelta<T0, T1, T2, T3, T4, T5, T6, >>.
Tuple7HalfDelta
Half-delta for (T0, T1, T2, T3, T4, T5, T6, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, T4, T5, T6, )>.
Tuple8Delta
Delta for (T0, T1, T2, T3, T4, T5, T6, T7, ). Alias for Delta<Tuple8HalfDelta<T0, T1, T2, T3, T4, T5, T6, T7, >>.
Tuple8HalfDelta
Half-delta for (T0, T1, T2, T3, T4, T5, T6, T7, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, T4, T5, T6, T7, )>.
Tuple9Delta
Delta for (T0, T1, T2, T3, T4, T5, T6, T7, T8, ). Alias for Delta<Tuple9HalfDelta<T0, T1, T2, T3, T4, T5, T6, T7, T8, >>.
Tuple9HalfDelta
Half-delta for (T0, T1, T2, T3, T4, T5, T6, T7, T8, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, T4, T5, T6, T7, T8, )>.
Tuple10Delta
Delta for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, ). Alias for Delta<Tuple10HalfDelta<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, >>.
Tuple10HalfDelta
Half-delta for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, )>.
Tuple11Delta
Delta for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, ). Alias for Delta<Tuple11HalfDelta<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, >>.
Tuple11HalfDelta
Half-delta for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, )>.
Tuple12Delta
Delta for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, ). Alias for Delta<Tuple12HalfDelta<T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, >>.
Tuple12HalfDelta
Half-delta for (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, ). Alias for BTreeMap<usize, (T0, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, )>.
U8Delta
Delta for u8. Alias for Delta<U8HalfDelta>.
U8HalfDelta
Half-delta for u8. Alias for BTreeMap<usize, u8>.
U16Delta
Delta for u16. Alias for Delta<U16HalfDelta>.
U16HalfDelta
Half-delta for u16. Alias for BTreeMap<usize, u16>.
U32Delta
Delta for u32. Alias for Delta<U32HalfDelta>.
U32HalfDelta
Half-delta for u32. Alias for BTreeMap<usize, u32>.
U64Delta
Delta for u64. Alias for Delta<U64HalfDelta>.
U64HalfDelta
Half-delta for u64. Alias for BTreeMap<usize, u64>.
U128Delta
Delta for u128. Alias for Delta<U128HalfDelta>.
U128HalfDelta
Half-delta for u128. Alias for BTreeMap<usize, u128>.
UnitDelta
Delta for (). Alias for Delta<UnitHalfDelta>.
UnitHalfDelta
Half-delta for (). Alias for BTreeMap<usize, ()>.
UsizeDelta
Delta for usize. Alias for Delta<UsizeHalfDelta>.
UsizeHalfDelta
Half-delta for usize. Alias for BTreeMap<usize, usize>.
VecDelta
Delta for Vec<V>. Alias for Delta<VecHalfDelta<V>>.
VecHalfDelta
Half-delta for Vec<V>. Alias for BTreeMap<usize, V>.

Derive Macros§

ApplyDeltaderive
Generates an impl of the trait ApplyDelta.
Container
Deltaderive
Generates an impl of the trait ApplyDelta.
FlushDeltaderive
Generates an impl of the trait FlushDelta.
HalfDeltaderive
Generates a half-delta type for the given struct or enum.