Skip to content

Commit c0c3da1

Browse files
committed
Add undo_2
1 parent 7681b25 commit c0c3da1

File tree

12 files changed

+3481
-0
lines changed

12 files changed

+3481
-0
lines changed

crates/bevy_text/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@ serde = { version = "1", features = ["derive"] }
4040
smallvec = { version = "1", default-features = false }
4141
sys-locale = "0.3.0"
4242
tracing = { version = "0.1", default-features = false, features = ["std"] }
43+
document-features = "0.2"
4344

4445
[dev-dependencies]
4546
approx = "0.5.1"
47+
serde_json = "1"
4648

4749
[lints]
4850
workspace = true

crates/bevy_text/src/undo_2/README.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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

Comments
 (0)