Undos (#60)
First draft
diff --git a/src/lib.rs b/src/lib.rs
index c5c20c6..3fd7620 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -39,6 +39,7 @@
mod kill_ring;
pub mod line_buffer;
pub mod config;
+mod undo;
mod tty;
diff --git a/src/line_buffer.rs b/src/line_buffer.rs
index 2d3dfec..46e28da 100644
--- a/src/line_buffer.rs
+++ b/src/line_buffer.rs
@@ -564,7 +564,7 @@
self.pos = start + text.len();
}
- fn insert_str(&mut self, idx: usize, s: &str) -> bool {
+ pub fn insert_str(&mut self, idx: usize, s: &str) -> bool {
if idx == self.buf.len() {
self.buf.push_str(s);
true
@@ -574,6 +574,10 @@
}
}
+ pub fn delete_range(&mut self, range: Range<usize>) {
+ self.buf.drain(range);
+ }
+
pub fn copy(&self, mvt: Movement) -> Option<String> {
if self.is_empty() {
return None;
diff --git a/src/undo.rs b/src/undo.rs
new file mode 100644
index 0000000..04a9db3
--- /dev/null
+++ b/src/undo.rs
@@ -0,0 +1,226 @@
+use line_buffer::LineBuffer;
+
+enum Action {
+ Insert(String), // QuotedInsert, SelfInsert, Yank
+ Delete(String), /* BackwardDeleteChar, BackwardKillWord, DeleteChar, KillLine, KillWholeLine, KillWord, UnixLikeDiscard, ViDeleteTo */
+ Replace(String, String), /* CapitalizeWord, Complete, DowncaseWord, Replace, TransposeChars, TransposeWords, UpcaseWord, YankPop */
+}
+
+struct Change {
+ idx: usize, // where the change happens
+ action: Action,
+}
+
+impl Change {
+ fn undo(&self, line: &mut LineBuffer) {
+ match self.action {
+ Action::Insert(ref text) => {
+ line.delete_range(self.idx..self.idx + text.len());
+ }
+ Action::Delete(ref text) => {
+ line.insert_str(self.idx, text);
+ }
+ Action::Replace(ref old, ref new) => {
+ line.replace(self.idx..self.idx + new.len(), old);
+ }
+ }
+ }
+
+ fn redo(&self, line: &mut LineBuffer) {
+ match self.action {
+ Action::Insert(ref text) => {
+ line.insert_str(self.idx, text);
+ }
+ Action::Delete(ref text) => {
+ line.delete_range(self.idx..self.idx + text.len());
+ }
+ Action::Replace(ref old, ref new) => {
+ line.replace(self.idx..self.idx + old.len(), new);
+ }
+ }
+ }
+
+ fn seq(&self, idx: usize) -> bool {
+ if let Action::Insert(ref text) = self.action {
+ self.idx + text.len() == idx
+ } else {
+ false
+ }
+ }
+}
+
+pub struct Changeset {
+ undos: Vec<Change>, // undoable changes
+ redos: Vec<Change>, // undone changes, redoable
+}
+
+impl Changeset {
+ pub fn new() -> Changeset {
+ Changeset {
+ undos: Vec::new(),
+ redos: Vec::new(),
+ }
+ }
+
+ fn insert_char(idx: usize, c: char) -> Change {
+ let mut text = String::new();
+ text.push(c);
+ Change {
+ idx: idx,
+ action: Action::Insert(text),
+ }
+ }
+
+ pub fn insert(&mut self, idx: usize, c: char) {
+ self.redos.clear();
+ let last_change = self.undos.pop();
+ match last_change {
+ Some(last_change) => {
+ // merge consecutive char insertions when char is alphanumeric
+ if c.is_alphanumeric() && last_change.seq(idx) {
+ let mut last_change = last_change;
+ if let Action::Insert(ref mut text) = last_change.action {
+ text.push(c);
+ } else {
+ unreachable!();
+ }
+ self.undos.push(last_change);
+ } else {
+ self.undos.push(last_change);
+ self.undos.push(Self::insert_char(idx, c));
+ }
+ }
+ None => {
+ self.undos.push(Self::insert_char(idx, c));
+ }
+ };
+ }
+
+ pub fn insert_str(&mut self, idx: usize, string: &str) {
+ self.redos.clear();
+ self.undos.push(Change {
+ idx: idx,
+ action: Action::Insert(string.to_string()),
+ });
+ }
+
+ pub fn delete(&mut self, idx: usize, string: String) {
+ self.redos.clear();
+ // TODO merge consecutive deletions
+ self.undos.push(Change {
+ idx: idx,
+ action: Action::Delete(string),
+ });
+ }
+
+ pub fn replace(&mut self, idx: usize, old: String, new: &str) {
+ self.redos.clear();
+ self.undos.push(Change {
+ idx: idx,
+ action: Action::Replace(old, new.to_string()),
+ });
+ }
+
+ pub fn undo(&mut self, line: &mut LineBuffer) -> bool {
+ match self.undos.pop() {
+ Some(change) => {
+ change.undo(line);
+ self.redos.push(change);
+ true
+ }
+ None => false,
+ }
+ }
+
+ pub fn redo(&mut self, line: &mut LineBuffer) -> bool {
+ match self.redos.pop() {
+ Some(change) => {
+ change.redo(line);
+ self.undos.push(change);
+ true
+ }
+ None => false,
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::Changeset;
+ use line_buffer::LineBuffer;
+
+ #[test]
+ fn test_insert_chars() {
+ let mut cs = Changeset::new();
+ cs.insert(0, 'H');
+ cs.insert(1, 'i');
+ assert_eq!(1, cs.undos.len());
+ assert_eq!(0, cs.redos.len());
+ cs.insert(0, ' ');
+ assert_eq!(2, cs.undos.len());
+ }
+
+ #[test]
+ fn test_insert_strings() {
+ let mut cs = Changeset::new();
+ cs.insert_str(0, "Hello");
+ cs.insert_str(5, ", ");
+ assert_eq!(2, cs.undos.len());
+ assert_eq!(0, cs.redos.len());
+ }
+
+ #[test]
+ fn test_undo_insert() {
+ let mut buf = LineBuffer::init("", 0);
+ buf.insert_str(0, "Hello");
+ buf.insert_str(5, ", world!");
+ let mut cs = Changeset::new();
+ assert_eq!(buf.as_str(), "Hello, world!");
+
+ cs.insert_str(5, ", world!");
+
+ cs.undo(&mut buf);
+ assert_eq!(0, cs.undos.len());
+ assert_eq!(1, cs.redos.len());
+ assert_eq!(buf.as_str(), "Hello");
+
+ cs.redo(&mut buf);
+ assert_eq!(1, cs.undos.len());
+ assert_eq!(0, cs.redos.len());
+ assert_eq!(buf.as_str(), "Hello, world!");
+ }
+
+ #[test]
+ fn test_undo_delete() {
+ let mut buf = LineBuffer::init("", 0);
+ buf.insert_str(0, "Hello");
+ let mut cs = Changeset::new();
+ assert_eq!(buf.as_str(), "Hello");
+
+ cs.delete(5, ", world!".to_string());
+
+ cs.undo(&mut buf);
+ assert_eq!(buf.as_str(), "Hello, world!");
+
+ cs.redo(&mut buf);
+ assert_eq!(buf.as_str(), "Hello");
+ }
+
+ #[test]
+ fn test_undo_replace() {
+ let mut buf = LineBuffer::init("", 0);
+ buf.insert_str(0, "Hello, world!");
+ let mut cs = Changeset::new();
+ assert_eq!(buf.as_str(), "Hello, world!");
+
+ buf.replace(1..5, "i");
+ assert_eq!(buf.as_str(), "Hi, world!");
+ cs.replace(1, "ello".to_string(), "i");
+
+ cs.undo(&mut buf);
+ assert_eq!(buf.as_str(), "Hello, world!");
+
+ cs.redo(&mut buf);
+ assert_eq!(buf.as_str(), "Hi, world!");
+ }
+}