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!");
+    }
+}