on master help sign in
Undo tree draft.

We keep a tree of transactions. This allows for persistent undo by simply serializing the changesets.

commitfd311fb8ad8792c8f21ed0bcf56d4a818715d5f7 authorBlaž Hrastnik <blaz@mxxn.io> dateSun 4 Oct 2020, 17:15 (6 years ago) parent9a73d3f
7 files changed+148−3
helix-core/src/history.rs+127 helix-core/src/lib.rs+2 helix-core/src/state.rs+1−1 helix-core/src/transaction.rs+1−1 helix-view/src/commands.rs+12 helix-view/src/keymap.rs+2 helix-view/src/view.rs+3−1
helix-core/src/history.rsnew file+127
@@ -0,0 +1,127 @@
1+use crate::{ChangeSet, Rope, State, Transaction};
2+
3+/// Undo-tree style history store.
4+pub struct History {
5+ revisions: Vec<Revision>,
6+ cursor: usize,
7+ //
8+}
9+
10+#[derive(Debug)]
11+struct Revision {
12+ // prev: usize,
13+ parent: usize,
14+ /// The transaction to revert to previous state.
15+ revert: Transaction,
16+ // selection before, selection after?
17+}
18+
19+impl Default for History {
20+ fn default() -> Self {
21+ // Add a dummy root revision with empty transaction
22+ Self {
23+ revisions: vec![Revision {
24+ parent: 0,
25+ revert: Transaction::from(ChangeSet::new(&Rope::new())),
26+ }],
27+ cursor: 0,
28+ }
29+ }
30+}
31+
32+impl History {
33+ pub fn commit_revision(&mut self, transaction: &Transaction, original: &State) {
34+ // TODO: store both directions
35+ // TODO: could store a single set if deletes also stored the text they delete
36+ let revert = transaction.invert(original);
37+
38+ let new_cursor = self.revisions.len();
39+ self.revisions.push(Revision {
40+ parent: self.cursor,
41+ revert,
42+ });
43+ self.cursor = new_cursor;
44+
45+ // TODO: child tracking too?
46+ }
47+
48+ #[inline]
49+ pub fn at_root(&self) -> bool {
50+ self.cursor == 0
51+ }
52+
53+ pub fn undo(&mut self, state: &mut State) {
54+ if self.at_root() {
55+ // We're at the root of undo, nothing to do.
56+ return;
57+ }
58+
59+ let current_revision = &self.revisions[self.cursor];
60+ // unimplemented!("{:?}", revision);
61+ // unimplemented!("{:?}", state.doc().len_chars());
62+ // TODO: pass the return value through?
63+ let success = current_revision.revert.apply(state);
64+
65+ if !success {
66+ panic!("Failed to apply undo!");
67+ }
68+
69+ self.cursor = current_revision.parent;
70+ }
71+
72+ pub fn redo(&mut self, state: &mut State) {
73+ let current_revision = &self.revisions[self.cursor];
74+
75+ // TODO: pick the latest child
76+
77+ // if !success {
78+ // panic!("Failed to apply undo!");
79+ // }
80+
81+ unimplemented!()
82+ }
83+}
84+
85+#[cfg(test)]
86+mod test {
87+ use super::*;
88+
89+ #[test]
90+ fn test_undo_redo() {
91+ let mut history = History::default();
92+ let doc = Rope::from("hello");
93+ let mut state = State::new(doc);
94+
95+ let transaction1 =
96+ Transaction::change(&state, vec![(5, 5, Some(" world!".into()))].into_iter());
97+
98+ // Need to commit before applying!
99+ history.commit_revision(&transaction1, &state);
100+ transaction1.apply(&mut state);
101+ assert_eq!("hello world!", state.doc());
102+
103+ // ---
104+
105+ let transaction2 =
106+ Transaction::change(&state, vec![(6, 11, Some("世界".into()))].into_iter());
107+
108+ // Need to commit before applying!
109+ history.commit_revision(&transaction2, &state);
110+ transaction2.apply(&mut state);
111+ assert_eq!("hello 世界!", state.doc());
112+
113+ // ---
114+
115+ history.undo(&mut state);
116+ assert_eq!("hello world!", state.doc());
117+ history.redo(&mut state);
118+ assert_eq!("hello 世界!", state.doc());
119+ history.undo(&mut state);
120+ history.undo(&mut state);
121+ assert_eq!("hello", state.doc());
122+
123+ // undo at root is a no-op
124+ history.undo(&mut state);
125+ assert_eq!("hello", state.doc());
126+ }
127+}
helix-core/src/lib.rs+2
@@ -1,5 +1,6 @@
11#![allow(unused)]
22pub mod graphemes;
3+mod history;
34pub mod macros;
45mod position;
56pub mod selection;
@@ -19,6 +20,7 @@ pub use selection::Range;
1920pub use selection::Selection;
2021pub use syntax::Syntax;
2122
23+pub use history::History;
2224pub use state::State;
2325
2426pub use transaction::{Assoc, Change, ChangeSet, Transaction};
helix-core/src/state.rs+1−1
@@ -48,8 +48,8 @@ impl State {
4848 doc,
4949 selection: Selection::single(0, 0),
5050 mode: Mode::Normal,
51− syntax: None,
5251 restore_cursor: false,
52+ syntax: None,
5353 }
5454 }
5555
helix-core/src/transaction.rs+1−1
@@ -338,7 +338,7 @@ impl ChangeSet {
338338
339339/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
340340/// a single transaction.
341−#[derive(Clone)]
341+#[derive(Debug, Clone)]
342342pub struct Transaction {
343343 /// Changes made to the buffer.
344344 pub(crate) changes: ChangeSet,
helix-view/src/commands.rs+12
@@ -417,3 +417,15 @@ pub fn delete_char_forward(view: &mut View, count: usize) {
417417 transaction.apply(&mut view.state);
418418 // TODO: need to store into history if successful
419419}
420+
421+// Undo / Redo
422+
423+pub fn undo(view: &mut View, _count: usize) {
424+ view.history.undo(&mut view.state);
425+
426+ // TODO: each command should simply return a Option<transaction>, then the higher level handles storing it?
427+}
428+
429+pub fn redo(view: &mut View, _count: usize) {
430+ view.history.redo(&mut view.state);
431+}
helix-view/src/keymap.rs+2
@@ -145,6 +145,8 @@ pub fn default() -> Keymaps {
145145 vec![key!('c')] => commands::change_selection,
146146 vec![key!('s')] => commands::split_selection_on_newline,
147147 vec![key!(';')] => commands::collapse_selection,
148+ vec![key!('u')] => commands::undo,
149+ vec![shift!('U')] => commands::redo,
148150 vec![Key {
149151 code: KeyCode::Esc,
150152 modifiers: Modifiers::NONE
helix-view/src/view.rs+3−1
@@ -5,12 +5,13 @@ use std::{borrow::Cow, path::PathBuf};
55use crate::theme::Theme;
66use helix_core::{
77 graphemes::{grapheme_width, RopeGraphemes},
8− Position, RopeSlice, State,
8+ History, Position, RopeSlice, State,
99};
1010use tui::layout::Rect;
1111
1212pub struct View {
1313 pub state: State,
14+ pub history: History,
1415 pub first_line: usize,
1516 pub size: (u16, u16),
1617 pub theme: Theme, // TODO: share one instance
@@ -26,6 +27,7 @@ impl View {
2627 first_line: 0,
2728 size, // TODO: pass in from term
2829 theme,
30+ history: History::default(),
2931 };
3032
3133 Ok(view)