Undo done the right way!

Introduction

An undo crate that makes it so that, the instant you edit something you undid to, instead of truncating the undo/redo history, it bakes the rewind onto the end of the Undo history as a precursor to your new change. The idea originates from (and all the credits goes to) [zaboople/klonk]. This crate is an implementation of this idea with a minor variation explained below.

As an example consider the following sequence of commands:

| Command | State | | ------- | ----- | | Init | | | Do A | A | | Do B | A, B | | Undo | A | | Do C | A, C |

With the classical undo, repeated undo would lead to the sequence:

| Command | State | |---------|-------| | | A, C | | Undo | A | | Undo | |

Starting from 5, with undo_2, repeating undo would lead to the sequence:

| Command | State | |---------|-------| | | A, C | | Undo | A | | Undo | A,B | | Undo | A | | Undo | |

undo_2's undo navigates back in history, while classical undo navigates back through the sequence of command that builds the state.

Features

  1. historical undo sequence, no commands are lost.
  2. user-friendly compared to complex undo trees.
  3. optimized implementation: no commands are ever copied.
  4. very lightweight, dumb and simple.
  5. possibility to merge and splice commands.

How to use it

Add the dependency to the cargo file:

[toml] [dependencies] undo_2 = "0.1"

Then add this to your source file:

ignore use undo_2::Commands;

The example below implements a dumb text editor. undo_2 does not perform itself "undos" and "redos", rather, it returns a sequence of commands that must be interpreted by the application. This design pattern makes implementation easier because it is not necessary to borrow data within the stored list of commands.

``` use undo_2::{Commands,Action};

enum Command { Add(char), Delete(char), }

struct TextEditor { text: String, command: Commands, }

impl TextEditor { pub fn new() -> Self { Self { text: String::new(), command: Commands::new(), } } pub fn addchar(&mut self, c: char) { self.text.push(c); self.command.push(Command::Add(c)); } pub fn deletechar(&mut self) { if let Some(c) = self.text.pop() { self.command.push(Command::Delete(c)); } } pub fn undo(&mut self) { for action in self.command.undo() { interpretaction(&mut self.text, action) } } pub fn redo(&mut self) { for action in self.command.redo() { interpretaction(&mut self.text, action) } } }

fn interpretaction(data: &mut String, action: Action<&Command>) { use Command::*; match action { Action::Do(Add(c)) | Action::Undo(Delete(c)) => { data.push(*c); } Action::Undo(Add()) | Action::Do(Delete(_)) => { data.pop(); } } }

let mut editor = TextEditor::new(); editor.addchar('a'); // :[1] editor.addchar('b'); // :[2] editor.addchar('d'); // :[3] asserteq!(editor.text, "abd");

editor.undo(); // first undo :[4] assert_eq!(editor.text, "ab");

editor.addchar('c'); // :[5] asserteq!(editor.text, "abc");

editor.undo(); // Undo [5] :[6] asserteq!(editor.text, "ab"); editor.undo(); // Undo the undo [4] :[7] asserteq!(editor.text, "abd"); editor.undo(); // Undo [3] :[8] asserteq!(editor.text, "ab"); editor.undo(); // Undo [2] :[9] asserteq!(editor.text, "a"); ```

More information

  1. After a sequence of consecutive undo, if a new command is added, the undos forming the sequence are merged. This makes the traversal of the undo sequence more concise by avoiding state duplication.

| Command | State | Comment | |---------|------- |----------------------| | Init | | | | Do A | A | | | Do B | A,B | | | Do C | A, B, C | | | Undo | A, B |Merged | | Undo | A |Merged | | Do D | A, D | | | Undo | A |Redo the 2 Merged Undo| | Undo | A, B, C | | | Undo | A, B | | | Undo | A | | | Undo | | |

  1. Each execution of an undos or redo may lead to the execution of a sequence of actions in the form Undo(a)+Do(b)+Do(c). Basic arithmetic is implemented assuming that Do(a)+Undo(a) is equivalent to not doing anything (here the 2 a's designate the same entity, not to equal objects).

The piece of code below, which is the longer version of the code above, illustrates points 1 and 2.

```ignore let mut editor = TextEditor::new(); editor.addchar('a'); // :[1] editor.addchar('b'); // :[2] editor.addchar('d'); // :[3] asserteq!(editor.text, "abd");

editor.undo(); // first undo :[4] assert_eq!(editor.text, "ab");

editor.addchar('c'); // :[5] asserteq!(editor.text, "abc");

editor.undo(); // Undo [5] :[6] asserteq!(editor.text, "ab"); editor.undo(); // Undo the undo [4] :[7] asserteq!(editor.text, "abd"); editor.undo(); // Undo [3] :[8] asserteq!(editor.text, "ab"); editor.undo(); // Undo [2] :[9] asserteq!(editor.text, "a");

editor.addchar('z'); // :[10] asserteq!(editor.text, "az"); // when an action is performed after a sequence // of undo, the undos are merged: undos [6] to [9] are merged now

editor.undo(); // back to [10] asserteq!(editor.text, "a"); editor.undo(); // back to [5]: reverses the consecutive sequence of undos in batch asserteq!(editor.text, "abc"); editor.undo(); // back to [4] asserteq!(editor.text, "ab"); editor.undo(); // back to [3] asserteq!(editor.text, "abd"); editor.undo(); // back to [2] asserteq!(editor.text, "ab"); editor.undo(); // back to [1] asserteq!(editor.text, "a"); editor.undo(); // back to [0] assert_eq!(editor.text, "");

editor.redo(); // back to [1] asserteq!(editor.text, "a"); editor.redo(); // back to [2] asserteq!(editor.text, "ab"); editor.redo(); // back to [3] asserteq!(editor.text, "abd"); editor.redo(); // back to [4] asserteq!(editor.text, "ab"); editor.redo(); // back to [5] asserteq!(editor.text, "abc"); editor.redo(); // back to [9]: redo inner consecutive sequence of undos in batch // (undo are merged only when they are not the last action) asserteq!(editor.text, "a"); editor.redo(); // back to [10] assert_eq!(editor.text, "az");

editor.addchar('1'); editor.addchar('2'); asserteq!(editor.text, "az12"); editor.undo(); editor.undo(); asserteq!(editor.text, "az"); editor.redo(); // undo is the last action, undo the undo only once asserteq!(editor.text, "az1"); editor.redo(); asserteq!(editor.text, "az12"); ```

Crate features

serde: enabled by default