An undo crate where 'undo' goes back in time sequentialy: no commands are lost. In practice, it undoes the "undo". For a detailed explanation (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.
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"); ```
| 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 | | |
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"); ```
serde
: enabled by default