Crate undoredo

Crate undoredo 

Source
Expand description

CI Status Docs Crates.io MIT OR Apache 2.0

§undoredo

undoredo is an undo-redo Rust library that wraps a collection inside a recorder decorator that observes the incoming insertions, removals and pushes while recording the changes in a reversible incremental diff structure.

This approach makes undoredo easier to use than other undo-redo libraries. Storing incremental diffs typically results in much more succint and reliable code than the commonly used Command pattern, which is what the popular and venerable undo and undo_2 crates use. The programmer is relieved from having to maintain application-specific implementations of commands, often complicated and prone to elusive runtime bugs, on which the Command pattern has to operate.

This library is no_std-compatible and has no mandatory third-party dependencies except for alloc. For ease of use, undoredo has convenience implementations for standard library collections: HashMap, HashSet, BTreeMap, BTreeSet, and for some third-party feature-gated types: StableVec, thunderdome::Arena, rstar::RTree, rstared::RTreed (read more in the Supported collections section).

§Usage

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

[dependencies]
undoredo = "0.3"

§Basic usage

Following is a basic usage example of undoredo over HashMap. You can find more examples in the examples/ directory.

use std::collections::HashMap;
use undoredo::{Insert, Recorder, UndoRedo};

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

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

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

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

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

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

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

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

    // The elements are back in the collection; the action has been redone.
    assert!(*recorder.collection() == HashMap::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 collection.
    let (mut hashmap, ..) = recorder.dissolve();
    assert!(hashmap == HashMap::from([(1, 'A'), (2, 'B'), (3, 'C')]));
}

§Storing and accessing command metadata along with edits

It is often desirable to store some metadata along with every recorded edit, usually a 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::HashMap;
use undoredo::{Insert, Recorder, UndoRedo};

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

fn main() {
    let mut recorder: Recorder<usize, char, HashMap<usize, char>> = Recorder::new(HashMap::new());
    let mut undoredo: UndoRedo<HashMap<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 edit.
    undoredo.cmd_commit(Command::PushChar, recorder.flush());

    // `Command::PushChar` is now the top element of the stack of done cmd-edits.
    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-edits.
    assert_eq!(undoredo.undone().last().unwrap().cmd, Command::PushChar);
}

§Undo-redo on maps with pushing

Some data structures with map semantics also provide a special type of insertion where a value is inserted without specifying a key, which the structure instead automatically generates and returns by itself. This operation is called “pushing”.

If a supported type has 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. examples/stable_vec.rs and examples/thunderdome.rs for complete examples of their usage.

§Undo-redo on sets

Some data structures have set semantics: they operate only on values, without exposing any usable notion of key or index. undoredo can provide its functionality to 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<usize, char, BTreeSet<char, ()>> = Recorder::new(BTreeSet::new());
let mut undoredo: UndoRedo<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 an instance of a data structure with 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 that is already present in a multiset.

§Undo-redo on custom types

To make undoredo work with a map-like 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).

§Supported collections

§Standard library

Rust’s standard library maps and sets are supported via built-in convenience implementations:

  • HashMap, gated by the std feature (enabled by default);
  • HashSet, gated by the std feature (enabled by default);
  • BTreeMap, not feature-gated;
  • BTreeSet, not feature-gated.

§Third-party types

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

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

[dependencies]
undoredo = { version = "0.3", features = ["stable-vec", "thunderdome", "rstar", "rstared"] }

§Unsupported collections

Some collections cannot be supported because they lack an interface on which maplike’s traits could be implemented. See the Unsupported collections section in maplike’s documentation for details.

§Contributing

We welcome issues and pull requests from anyone both to our canonical repository on Codeberg and to our GitHub mirror.

§Licence

undoredo is dual-licensed as under either of

at your option.

Structs§

Edit
A reversible set of changes to a collection.
Recorder
Records edits applied to a collection so they can be replayed or reverted.
UndoRedo
An undo-redo bistack.

Traits§

ApplyEdit
Apply the changes in an edit to a collection.
Get
Returns a reference to the value corresponding to the key.
Insert
Insert a key-value pair into the collection.
IntoIter
Consume the collection and yield owned key-value pairs.
Push
Insert a value into the collection without specifying a key, returning the key that was automatically generated.
Remove
Remove a key from the collection, returning the value at the key if the key was previously in the map.