|
| 1 | +# Undo done the right way! |
| 2 | + |
| 3 | +Via https://gitlab.com/okannen/undo_2/-/tree/b32c34edb2c15c266b946f0d82188624f3aa3fdc |
| 4 | + |
| 5 | +## Introduction |
| 6 | + |
| 7 | +An undo crate that makes it so that, the instant you edit something you undid |
| 8 | +to, instead of truncating the undo/redo history, it bakes the rewind onto the |
| 9 | +end of the Undo history as a precursor to your new change. I found the idea in |
| 10 | +[zaboople/klong][zaboople]. This crate is an implementation |
| 11 | +of this idea with a minor variation explained below. |
| 12 | + |
| 13 | +As an example consider the following sequence of commands: |
| 14 | + |
| 15 | +| Command | State | |
| 16 | +| ------- | ----- | |
| 17 | +| Init | | |
| 18 | +| Do A | A | |
| 19 | +| Do B | A, B | |
| 20 | +| Undo | A | |
| 21 | +| Do C | A, C | |
| 22 | + |
| 23 | +With the **classical undo**, repeated undo would lead to the sequence: |
| 24 | + |
| 25 | +| Command | State | |
| 26 | +|---------|-------| |
| 27 | +| | A, C | |
| 28 | +| Undo | A | |
| 29 | +| Undo | | |
| 30 | + |
| 31 | + |
| 32 | +Starting from 5, with **undo_2**, repeating undo would lead to the sequence: |
| 33 | + |
| 34 | +| Command | State | |
| 35 | +|---------|-------| |
| 36 | +| | A, C | |
| 37 | +| Undo | A | |
| 38 | +| Undo | A,B | |
| 39 | +| Undo | A | |
| 40 | +| Undo | | |
| 41 | + |
| 42 | +**undo_2**'s undo navigates back in history, while classical undo navigates back |
| 43 | +through the sequence of command that builds the state. |
| 44 | + |
| 45 | +This is actualy the way undo is often implemented in mac's (cocoa library), emacs |
| 46 | +and it is similar to vim :earlier. |
| 47 | + |
| 48 | +## Features |
| 49 | + |
| 50 | + 1. historical undo sequence, no commands are lost. |
| 51 | + 2. user-friendly compared to complex undo trees. |
| 52 | + 3. optimized implementation: no commands are ever copied. |
| 53 | + 4. very lightweight, dumb and simple. |
| 54 | + 5. possibility to merge and splice commands. |
| 55 | + |
| 56 | +## How to use it |
| 57 | + |
| 58 | +Add the dependency to the cargo file: |
| 59 | + |
| 60 | +```[toml] |
| 61 | +[dependencies] |
| 62 | +undo_2 = "0.1" |
| 63 | +``` |
| 64 | + |
| 65 | +Then add this to your source file: |
| 66 | + |
| 67 | +```ignore |
| 68 | +use undo_2::Commands; |
| 69 | +``` |
| 70 | + |
| 71 | +The example below implements a dumb text editor. *undo_2* does not perform |
| 72 | +itself "undos" and "redos", rather, it returns a sequence of commands that must |
| 73 | +be interpreted by the application. This design pattern makes implementation |
| 74 | +easier because it is not necessary to borrow data within the stored list of |
| 75 | +commands. |
| 76 | + |
| 77 | +``` |
| 78 | +use undo_2::{Commands,Action}; |
| 79 | +use Action::{Do,Undo}; |
| 80 | +
|
| 81 | +enum Command { |
| 82 | + Add(char), |
| 83 | + Delete(char), |
| 84 | +} |
| 85 | +
|
| 86 | +struct TextEditor { |
| 87 | + text: String, |
| 88 | + command: Commands<Command>, |
| 89 | +} |
| 90 | +
|
| 91 | +impl TextEditor { |
| 92 | + pub fn new() -> Self { |
| 93 | + Self { |
| 94 | + text: String::new(), |
| 95 | + command: Commands::new(), |
| 96 | + } |
| 97 | + } |
| 98 | + pub fn add_char(&mut self, c: char) { |
| 99 | + self.text.push(c); |
| 100 | + self.command.push(Command::Add(c)); |
| 101 | + } |
| 102 | + pub fn delete_char(&mut self) { |
| 103 | + if let Some(c) = self.text.pop() { |
| 104 | + self.command.push(Command::Delete(c)); |
| 105 | + } |
| 106 | + } |
| 107 | + pub fn undo(&mut self) { |
| 108 | + for action in self.command.undo() { |
| 109 | + interpret_action(&mut self.text, action) |
| 110 | + } |
| 111 | + } |
| 112 | + pub fn redo(&mut self) { |
| 113 | + for action in self.command.redo() { |
| 114 | + interpret_action(&mut self.text, action) |
| 115 | + } |
| 116 | + } |
| 117 | +} |
| 118 | +
|
| 119 | +fn interpret_action(data: &mut String, action: Action<&Command>) { |
| 120 | + use Command::*; |
| 121 | + match action { |
| 122 | + Do(Add(c)) | Undo(Delete(c)) => { |
| 123 | + data.push(*c); |
| 124 | + } |
| 125 | + Undo(Add(_)) | Do(Delete(_)) => { |
| 126 | + data.pop(); |
| 127 | + } |
| 128 | + } |
| 129 | +} |
| 130 | +
|
| 131 | +let mut editor = TextEditor::new(); |
| 132 | +editor.add_char('a'); // :[1] |
| 133 | +editor.add_char('b'); // :[2] |
| 134 | +editor.add_char('d'); // :[3] |
| 135 | +assert_eq!(editor.text, "abd"); |
| 136 | +
|
| 137 | +editor.undo(); // first undo :[4] |
| 138 | +assert_eq!(editor.text, "ab"); |
| 139 | +
|
| 140 | +editor.add_char('c'); // :[5] |
| 141 | +assert_eq!(editor.text, "abc"); |
| 142 | +
|
| 143 | +editor.undo(); // Undo [5] :[6] |
| 144 | +assert_eq!(editor.text, "ab"); |
| 145 | +editor.undo(); // Undo the undo [4] :[7] |
| 146 | +assert_eq!(editor.text, "abd"); |
| 147 | +editor.undo(); // Undo [3] :[8] |
| 148 | +assert_eq!(editor.text, "ab"); |
| 149 | +editor.undo(); // Undo [2] :[9] |
| 150 | +assert_eq!(editor.text, "a"); |
| 151 | +``` |
| 152 | + |
| 153 | +## More information |
| 154 | + |
| 155 | +1. After a sequence of consecutive undo, if a new command is added, the undos |
| 156 | + forming the sequence are merged. This makes the traversal of the undo |
| 157 | + sequence more concise by avoiding state duplication. |
| 158 | + |
| 159 | +| Command | State | Comment | |
| 160 | +|---------|------- |----------------------| |
| 161 | +| Init | | | |
| 162 | +| Do A | A | | |
| 163 | +| Do B | A,B | | |
| 164 | +| Do C | A, B, C | | |
| 165 | +| Undo | A, B |Merged | |
| 166 | +| Undo | A |Merged | |
| 167 | +| Do D | A, D | | |
| 168 | +| Undo | A |Redo the 2 Merged Undo| |
| 169 | +| Undo | A, B, C | | |
| 170 | +| Undo | A, B | | |
| 171 | +| Undo | A | | |
| 172 | +| Undo | | | |
| 173 | + |
| 174 | +2. Each execution of an undos or redo may lead to the execution of a sequence of |
| 175 | + actions in the form `Undo(a)+Do(b)+Do(c)`. Basic arithmetic is implemented |
| 176 | + assuming that `Do(a)+Undo(a)` is equivalent to not doing anything (here the |
| 177 | + 2 `a`'s designate the same entity, not to equal objects). |
| 178 | + |
| 179 | +The piece of code below, which is the longer version of the code above, illustrates points 1 and 2. |
| 180 | + |
| 181 | +```ignore |
| 182 | +let mut editor = TextEditor::new(); |
| 183 | +editor.add_char('a'); // :[1] |
| 184 | +editor.add_char('b'); // :[2] |
| 185 | +editor.add_char('d'); // :[3] |
| 186 | +assert_eq!(editor.text, "abd"); |
| 187 | +
|
| 188 | +editor.undo(); // first undo :[4] |
| 189 | +assert_eq!(editor.text, "ab"); |
| 190 | +
|
| 191 | +editor.add_char('c'); // :[5] |
| 192 | +assert_eq!(editor.text, "abc"); |
| 193 | +
|
| 194 | +editor.undo(); // Undo [5] :[6] |
| 195 | +assert_eq!(editor.text, "ab"); |
| 196 | +editor.undo(); // Undo the undo [4] :[7] |
| 197 | +assert_eq!(editor.text, "abd"); |
| 198 | +editor.undo(); // Undo [3] :[8] |
| 199 | +assert_eq!(editor.text, "ab"); |
| 200 | +editor.undo(); // Undo [2] :[9] |
| 201 | +assert_eq!(editor.text, "a"); |
| 202 | +
|
| 203 | +editor.add_char('z'); // :[10] |
| 204 | +assert_eq!(editor.text, "az"); |
| 205 | +// when an action is performed after a sequence |
| 206 | +// of undo, the undos are merged: undos [6] to [9] are merged now |
| 207 | +
|
| 208 | +editor.undo(); // back to [10] |
| 209 | +assert_eq!(editor.text, "a"); |
| 210 | +editor.undo(); // back to [5]: reverses the consecutive sequence of undos in batch |
| 211 | +assert_eq!(editor.text, "abc"); |
| 212 | +editor.undo(); // back to [4] |
| 213 | +assert_eq!(editor.text, "ab"); |
| 214 | +editor.undo(); // back to [3] |
| 215 | +assert_eq!(editor.text, "abd"); |
| 216 | +editor.undo(); // back to [2] |
| 217 | +assert_eq!(editor.text, "ab"); |
| 218 | +editor.undo(); // back to [1] |
| 219 | +assert_eq!(editor.text, "a"); |
| 220 | +editor.undo(); // back to [0] |
| 221 | +assert_eq!(editor.text, ""); |
| 222 | +
|
| 223 | +editor.redo(); // back to [1] |
| 224 | +assert_eq!(editor.text, "a"); |
| 225 | +editor.redo(); // back to [2] |
| 226 | +assert_eq!(editor.text, "ab"); |
| 227 | +editor.redo(); // back to [3] |
| 228 | +assert_eq!(editor.text, "abd"); |
| 229 | +editor.redo(); // back to [4] |
| 230 | +assert_eq!(editor.text, "ab"); |
| 231 | +editor.redo(); // back to [5] |
| 232 | +assert_eq!(editor.text, "abc"); |
| 233 | +editor.redo(); // back to [9]: redo inner consecutive sequence of undos in batch |
| 234 | + // (undo are merged only when they are not the last action) |
| 235 | +assert_eq!(editor.text, "a"); |
| 236 | +editor.redo(); // back to [10] |
| 237 | +assert_eq!(editor.text, "az"); |
| 238 | +
|
| 239 | +editor.add_char('1'); |
| 240 | +editor.add_char('2'); |
| 241 | +assert_eq!(editor.text, "az12"); |
| 242 | +editor.undo(); |
| 243 | +editor.undo(); |
| 244 | +assert_eq!(editor.text, "az"); |
| 245 | +editor.redo(); // undo is the last action, undo the undo only once |
| 246 | +assert_eq!(editor.text, "az1"); |
| 247 | +editor.redo(); |
| 248 | +assert_eq!(editor.text, "az12"); |
| 249 | +``` |
| 250 | + |
| 251 | +## Release note |
| 252 | +### Version 0.3 |
| 253 | + - [Action] is now an enum taking commands, the list of command to be |
| 254 | + executed is of the form [Action<T>]; |
| 255 | + - added [Commands::can_undo] and [Commands::can_redo]; |
| 256 | + - added [Commands::rebuild], which correspond to the classical redo; |
| 257 | + - fixed a bug in [Commands::undo_or_redo_to_index] |
| 258 | + - Added support for special commands that represent a state setting. See [SetOrTransition]. |
| 259 | + |
| 260 | +[zaboople]: https://github.com/zaboople/klonk/blob/master/TheGURQ.md |
| 261 | + |
0 commit comments