From 5df4a69a1ea0078db6869b6a35002f76d1540cb9 Mon Sep 17 00:00:00 2001
From: gwenn <gtreguier@gmail.com>
Date: Tue, 29 Mar 2016 13:35:23 +0200
Subject: [PATCH] Kill-ring

---
 src/consts.rs    |   3 +
 src/kill_ring.rs | 238 +++++++++++++++++++++++++++++++++++++++++++++++
 src/lib.rs       | 200 ++++++++++++++++++++++++++++++---------
 3 files changed, 399 insertions(+), 42 deletions(-)
 create mode 100644 src/kill_ring.rs

diff --git a/src/consts.rs b/src/consts.rs
index d35a1625..17f01f01 100644
--- a/src/consts.rs
+++ b/src/consts.rs
@@ -23,10 +23,12 @@ pub enum KeyPress {
     CTRL_T,
     CTRL_U,
     CTRL_W,
+    CTRL_Y,
     ESC,
     BACKSPACE,
     UNKNOWN_ESC_SEQ,
     ESC_SEQ_DELETE,
+    ESC_Y,
 }
 
 pub fn char_to_key_press(c: char) -> KeyPress {
@@ -52,6 +54,7 @@ pub fn char_to_key_press(c: char) -> KeyPress {
         '\x14' => KeyPress::CTRL_T,
         '\x15' => KeyPress::CTRL_U,
         '\x17' => KeyPress::CTRL_W,
+        '\x19' => KeyPress::CTRL_Y,
         '\x1b' => KeyPress::ESC,
         '\x7f' => KeyPress::BACKSPACE,
         _ => KeyPress::NULL,
diff --git a/src/kill_ring.rs b/src/kill_ring.rs
new file mode 100644
index 00000000..289fc0f4
--- /dev/null
+++ b/src/kill_ring.rs
@@ -0,0 +1,238 @@
+//! Kill Ring
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum Action {
+    Kill,
+    Yank(usize),
+    Other,
+}
+
+pub struct KillRing {
+    slots: Vec<String>,
+    index: usize,
+    last_action: Action,
+}
+
+impl KillRing {
+    /// Create a new kill-ring of the given `size`.
+    pub fn new(size: usize) -> KillRing {
+        KillRing {
+            slots: Vec::with_capacity(size),
+            index: 0,
+            last_action: Action::Other,
+        }
+    }
+
+    /// Reset last_astion state.
+    pub fn reset(&mut self) {
+        self.last_action = Action::Other;
+    }
+
+    /// Add `text` to the kill-ring.
+    pub fn kill(&mut self, text: &str, forward: bool) {
+        match self.last_action {
+            Action::Kill => {
+                if self.slots.capacity() == 0 {
+                    // disabled
+                    return;
+                }
+                if forward {
+                    // append
+                    self.slots[self.index].push_str(text);
+                } else {
+                    // prepend
+                    self.slots[self.index] = String::from(text) + &self.slots[self.index];
+                }
+            }
+            _ => {
+                self.last_action = Action::Kill;
+                if self.slots.capacity() == 0 {
+                    // disabled
+                    return;
+                }
+                if self.index == self.slots.capacity() - 1 {
+                    // full
+                    self.index = 0;
+                } else if !self.slots.is_empty() {
+                    self.index += 1;
+                }
+                if self.index == self.slots.len() {
+                    self.slots.push(String::from(text))
+                } else {
+                    self.slots[self.index] = String::from(text);
+                }
+            }
+        }
+    }
+
+    /// Yank previously killed text.
+    /// Return `None` when kill-ring is empty.
+    pub fn yank(&mut self) -> Option<&String> {
+        if self.slots.len() == 0 {
+            None
+        } else {
+            self.last_action = Action::Yank(self.slots[self.index].len());
+            Some(&self.slots[self.index])
+        }
+    }
+
+    /// Yank killed text stored in previous slot.
+    /// Return `None` when the previous command was not a yank.
+    pub fn yank_pop(&mut self) -> Option<(usize, &String)> {
+        match self.last_action {
+            Action::Yank(yank_size) => {
+                if self.slots.len() == 0 {
+                    return None;
+                }
+                if self.index == 0 {
+                    self.index = self.slots.len() - 1;
+                } else {
+                    self.index -= 1;
+                }
+                self.last_action = Action::Yank(self.slots[self.index].len());
+                Some((yank_size, &self.slots[self.index]))
+            }
+            _ => None,
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::{Action, KillRing};
+
+    #[test]
+    fn disabled() {
+        let mut kill_ring = KillRing::new(0);
+        kill_ring.kill("text", true);
+        assert!(kill_ring.slots.is_empty());
+        assert_eq!(0, kill_ring.index);
+        assert_eq!(Action::Kill, kill_ring.last_action);
+
+        assert_eq!(None, kill_ring.yank());
+        assert_eq!(Action::Kill, kill_ring.last_action);
+    }
+
+    #[test]
+    fn one_kill() {
+        let mut kill_ring = KillRing::new(2);
+        kill_ring.kill("word1", true);
+        assert_eq!(0, kill_ring.index);
+        assert_eq!(1, kill_ring.slots.len());
+        assert_eq!("word1", kill_ring.slots[0]);
+        assert_eq!(Action::Kill, kill_ring.last_action);
+    }
+
+    #[test]
+    fn kill_kill_forward() {
+        let mut kill_ring = KillRing::new(2);
+        kill_ring.kill("word1", true);
+        kill_ring.kill(" word2", true);
+        assert_eq!(0, kill_ring.index);
+        assert_eq!(1, kill_ring.slots.len());
+        assert_eq!("word1 word2", kill_ring.slots[0]);
+        assert_eq!(Action::Kill, kill_ring.last_action);
+    }
+
+    #[test]
+    fn kill_kill_backward() {
+        let mut kill_ring = KillRing::new(2);
+        kill_ring.kill("word1", false);
+        kill_ring.kill("word2 ", false);
+        assert_eq!(0, kill_ring.index);
+        assert_eq!(1, kill_ring.slots.len());
+        assert_eq!("word2 word1", kill_ring.slots[0]);
+        assert_eq!(Action::Kill, kill_ring.last_action);
+    }
+
+    #[test]
+    fn kill_other_kill() {
+        let mut kill_ring = KillRing::new(2);
+        kill_ring.kill("word1", true);
+        kill_ring.reset();
+        kill_ring.kill("word2", true);
+        assert_eq!(1, kill_ring.index);
+        assert_eq!(2, kill_ring.slots.len());
+        assert_eq!("word1", kill_ring.slots[0]);
+        assert_eq!("word2", kill_ring.slots[1]);
+        assert_eq!(Action::Kill, kill_ring.last_action);
+    }
+
+    #[test]
+    fn many_kill() {
+        let mut kill_ring = KillRing::new(2);
+        kill_ring.kill("word1", true);
+        kill_ring.reset();
+        kill_ring.kill("word2", true);
+        kill_ring.reset();
+        kill_ring.kill("word3", true);
+        kill_ring.reset();
+        kill_ring.kill("word4", true);
+        assert_eq!(1, kill_ring.index);
+        assert_eq!(2, kill_ring.slots.len());
+        assert_eq!("word3", kill_ring.slots[0]);
+        assert_eq!("word4", kill_ring.slots[1]);
+        assert_eq!(Action::Kill, kill_ring.last_action);
+    }
+
+    #[test]
+    fn yank() {
+        let mut kill_ring = KillRing::new(2);
+        kill_ring.kill("word1", true);
+        kill_ring.reset();
+        kill_ring.kill("word2", true);
+
+        assert_eq!(Some(&"word2".to_string()), kill_ring.yank());
+        assert_eq!(Action::Yank(5), kill_ring.last_action);
+        assert_eq!(Some(&"word2".to_string()), kill_ring.yank());
+        assert_eq!(Action::Yank(5), kill_ring.last_action);
+    }
+
+    #[test]
+    fn yank_pop() {
+        let mut kill_ring = KillRing::new(2);
+        kill_ring.kill("word1", true);
+        kill_ring.reset();
+        kill_ring.kill("longword2", true);
+
+        assert_eq!(None, kill_ring.yank_pop());
+        kill_ring.yank();
+        assert_eq!(Some((9, &"word1".to_string())), kill_ring.yank_pop());
+        assert_eq!(Some((5, &"longword2".to_string())), kill_ring.yank_pop());
+        assert_eq!(Some((9, &"word1".to_string())), kill_ring.yank_pop());
+    }
+}
+
+// Ctrl-K -> delete to kill ring (forward) (killLine)
+// Ctrl-U -> erase to kill ring (backward) (resetLine)
+// Ctrl-W -> erase word to kill ring (backward) (unixWordRubout)
+//
+// Meta Ctrl-H -> deletePreviousWord
+// Meta Delete -> deletePreviousWord
+// Meta D -> deleteNextWord
+// Meta-Y/y -> yankPop
+//
+// Ctrl-Y -> paste from buffer (yank)
+//
+// resetLine
+// unixWordRubout
+// deletePreviousWord
+// deleteNextWord
+// killLine
+//
+// yank
+// yankPop
+//
+// echo hello world
+// Ctrl-W Ctrl-W Ctrl-Y
+// echo hello
+// echo
+// echo hello world
+//
+// echo hello world
+// Ctrl-W
+// echo hello rust
+// Ctrl-W Ctrl-Y Meta-Y
+// echo hello world
+// Meta-Y
+//
diff --git a/src/lib.rs b/src/lib.rs
index 21a1e000..39b92c19 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -26,6 +26,7 @@ pub mod completion;
 mod consts;
 pub mod error;
 pub mod history;
+mod kill_ring;
 
 use std::fmt;
 use std::io::{self, Read, Write};
@@ -37,6 +38,7 @@ use nix::sys::termios;
 use completion::Completer;
 use consts::{KeyPress, char_to_key_press};
 use history::History;
+use kill_ring::KillRing;
 
 /// The error type for I/O and Linux Syscalls (Errno)
 pub type Result<T> = result::Result<T, error::ReadlineError>;
@@ -259,17 +261,21 @@ fn width(s: &str) -> usize {
         let mut w = 0;
         let mut esc_seq = 0;
         for c in s.chars() {
-            if esc_seq  == 1 {
-                if c == '[' { // CSI
+            if esc_seq == 1 {
+                if c == '[' {
+                    // CSI
                     esc_seq = 2;
-                } else { // two-character sequence
+                } else {
+                    // two-character sequence
                     esc_seq = 0;
                 }
             } else if esc_seq == 2 {
                 if c == ';' || (c >= '0' && c <= '9') {
-                } else if c == 'm' { // last
+                } else if c == 'm' {
+                    // last
                     esc_seq = 0
-                } else { // not supported
+                } else {
+                    // not supported
                     w += unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
                     esc_seq = 0
                 }
@@ -316,6 +322,20 @@ fn edit_insert(s: &mut State, ch: char) -> Result<()> {
     }
 }
 
+fn edit_yank(s: &mut State, text: &str) -> Result<()> {
+    for ch in text.chars() {
+        try!(edit_insert(s, ch));
+    }
+    Ok(())
+}
+
+fn edit_yank_pop(s: &mut State, yank_size: usize, text: &str) -> Result<()> {
+    s.buf.drain((s.pos - yank_size)..s.pos);
+    s.pos -= yank_size;
+    try!(s.refresh_line());
+    edit_yank(s, text)
+}
+
 /// Move cursor on the left.
 fn edit_move_left(s: &mut State) -> Result<()> {
     if s.pos > 0 {
@@ -382,23 +402,25 @@ fn edit_backspace(s: &mut State) -> Result<()> {
 }
 
 /// Kill the text from point to the end of the line.
-fn edit_kill_line(s: &mut State) -> Result<()> {
+fn edit_kill_line(s: &mut State) -> Result<Option<String>> {
     if s.buf.len() > 0 && s.pos < s.buf.len() {
-        s.buf.drain(s.pos..);
-        s.refresh_line()
+        let text = s.buf.drain(s.pos..).collect();
+        try!(s.refresh_line());
+        Ok(Some(text))
     } else {
-        Ok(())
+        Ok(None)
     }
 }
 
 /// Kill backward from point to the beginning of the line.
-fn edit_discard_line(s: &mut State) -> Result<()> {
+fn edit_discard_line(s: &mut State) -> Result<Option<String>> {
     if s.pos > 0 && s.buf.len() > 0 {
-        s.buf.drain(..s.pos);
+        let text = s.buf.drain(..s.pos).collect();
         s.pos = 0;
-        s.refresh_line()
+        try!(s.refresh_line());
+        Ok(Some(text))
     } else {
-        Ok(())
+        Ok(None)
     }
 }
 
@@ -428,7 +450,7 @@ fn edit_transpose_chars(s: &mut State) -> Result<()> {
 
 /// Delete the previous word, maintaining the cursor at the start of the
 /// current word.
-fn edit_delete_prev_word(s: &mut State) -> Result<()> {
+fn edit_delete_prev_word(s: &mut State) -> Result<Option<String>> {
     if s.pos > 0 {
         let old_pos = s.pos;
         let mut ch = s.buf.char_at_reverse(s.pos);
@@ -442,10 +464,11 @@ fn edit_delete_prev_word(s: &mut State) -> Result<()> {
             s.pos -= ch.len_utf8();
             ch = s.buf.char_at_reverse(s.pos);
         }
-        s.buf.drain(s.pos..old_pos);
-        s.refresh_line()
+        let text = s.buf.drain(s.pos..old_pos).collect();
+        try!(s.refresh_line());
+        Ok(Some(text))
     } else {
-        Ok(())
+        Ok(None)
     }
 }
 
@@ -656,12 +679,16 @@ fn escape_sequence<R: io::Read>(chars: &mut io::Chars<R>) -> Result<KeyPress> {
         // TODO ESC-R (r): Undo all changes made to this line.
         // TODO EST-T (t): transpose words
         // TODO ESC-U (u): uppercase word after point
-        // TODO ESC-Y (y): yank-pop
         // TODO ESC-CTRl-H | ESC-BACKSPACE kill one word backward
         // TODO ESC-<: move to first entry in history
         // TODO ESC->: move to last entry in history
-        writeln!(io::stderr(), "key: {:?}, seq1, {:?}", KeyPress::ESC, seq1).unwrap();
-        Ok(KeyPress::UNKNOWN_ESC_SEQ)
+        match seq1 {
+            'y' | 'Y' => Ok(KeyPress::ESC_Y),
+            _ => {
+                writeln!(io::stderr(), "key: {:?}, seq1, {:?}", KeyPress::ESC, seq1).unwrap();
+                Ok(KeyPress::UNKNOWN_ESC_SEQ)
+            }
+        }
     }
 }
 
@@ -670,17 +697,20 @@ fn escape_sequence<R: io::Read>(chars: &mut io::Chars<R>) -> Result<KeyPress> {
 /// (e.g., C-c will exit readline)
 fn readline_edit(prompt: &str,
                  history: &mut History,
-                 completer: Option<&Completer>)
+                 completer: Option<&Completer>,
+                 kill_ring: &mut KillRing)
                  -> Result<String> {
     let mut stdout = io::stdout();
     try!(write_and_flush(&mut stdout, prompt.as_bytes()));
 
+    kill_ring.reset();
     let mut s = State::new(&mut stdout, prompt, MAX_LINE, get_columns(), history.len());
     let stdin = io::stdin();
     let mut chars = stdin.lock().chars();
     loop {
         let mut ch = try!(chars.next().unwrap()); // FIXME unwrap
         if !ch.is_control() {
+            kill_ring.reset();
             try!(edit_insert(&mut s, ch));
             continue;
         }
@@ -690,6 +720,7 @@ fn readline_edit(prompt: &str,
         if key == KeyPress::TAB && completer.is_some() {
             let next = try!(complete_line(&mut chars, &mut s, completer.unwrap()));
             if next.is_some() {
+                kill_ring.reset();
                 ch = next.unwrap();
                 if !ch.is_control() {
                     try!(edit_insert(&mut s, ch));
@@ -716,10 +747,22 @@ fn readline_edit(prompt: &str,
         }
 
         match key {
-            KeyPress::CTRL_A => try!(edit_move_home(&mut s)), // Move to the beginning of line.
-            KeyPress::CTRL_B => try!(edit_move_left(&mut s)), // Move back a character.
-            KeyPress::CTRL_C => return Err(error::ReadlineError::Interrupted),
+            KeyPress::CTRL_A => {
+                kill_ring.reset();
+                // Move to the beginning of line.
+                try!(edit_move_home(&mut s))
+            }
+            KeyPress::CTRL_B => {
+                kill_ring.reset();
+                // Move back a character.
+                try!(edit_move_left(&mut s))
+            }
+            KeyPress::CTRL_C => {
+                kill_ring.reset();
+                return Err(error::ReadlineError::Interrupted);
+            }
             KeyPress::CTRL_D => {
+                kill_ring.reset();
                 if s.buf.len() > 0 {
                     // Delete (forward) one character at point.
                     try!(edit_delete(&mut s))
@@ -727,33 +770,97 @@ fn readline_edit(prompt: &str,
                     return Err(error::ReadlineError::Eof);
                 }
             }
-            KeyPress::CTRL_E => try!(edit_move_end(&mut s)), // Move to the end of line.
-            KeyPress::CTRL_F => try!(edit_move_right(&mut s)), // Move forward a character.
-            KeyPress::CTRL_H | KeyPress::BACKSPACE => try!(edit_backspace(&mut s)), // Delete one character backward.
-            KeyPress::CTRL_J => break, // like ENTER
-            KeyPress::CTRL_K => try!(edit_kill_line(&mut s)), // Kill the text from point to the end of the line.
+            KeyPress::CTRL_E => {
+                kill_ring.reset();
+                // Move to the end of line.
+                try!(edit_move_end(&mut s))
+            }
+            KeyPress::CTRL_F => {
+                kill_ring.reset();
+                // Move forward a character.
+                try!(edit_move_right(&mut s))
+            }
+            KeyPress::CTRL_H | KeyPress::BACKSPACE => {
+                kill_ring.reset();
+                // Delete one character backward.
+                try!(edit_backspace(&mut s))
+            }
+            KeyPress::CTRL_J => {
+                // like ENTER
+                kill_ring.reset();
+                break;
+            }
+            KeyPress::CTRL_K => {
+                // Kill the text from point to the end of the line.
+                match try!(edit_kill_line(&mut s)) {
+                    Some(text) => kill_ring.kill(&text, true),
+                    None => (),
+                }
+            }
             KeyPress::CTRL_L => {
                 // Clear the screen leaving the current line at the top of the screen.
                 try!(clear_screen(s.out));
                 try!(s.refresh_line())
             }
             KeyPress::CTRL_N => {
+                kill_ring.reset();
                 // Fetch the next command from the history list.
                 try!(edit_history_next(&mut s, history, false))
             }
             KeyPress::CTRL_P => {
+                kill_ring.reset();
                 // Fetch the previous command from the history list.
                 try!(edit_history_next(&mut s, history, true))
             }
-            KeyPress::CTRL_T => try!(edit_transpose_chars(&mut s)), // Exchange the char before cursor with the character at cursor.
-            KeyPress::CTRL_U => try!(edit_discard_line(&mut s)), // Kill backward from point to the beginning of the line.
+            KeyPress::CTRL_T => {
+                kill_ring.reset();
+                // Exchange the char before cursor with the character at cursor.
+                try!(edit_transpose_chars(&mut s))
+            }
+            KeyPress::CTRL_U => {
+                // Kill backward from point to the beginning of the line.
+                match try!(edit_discard_line(&mut s)) {
+                    Some(text) => kill_ring.kill(&text, false),
+                    None => (),
+                }
+            }
             // TODO CTRL_V // Quoted insert
-            KeyPress::CTRL_W => try!(edit_delete_prev_word(&mut s)), // Kill the word behind point, using white space as a word boundary
-            // TODO CTRL_Y // retrieve (yank) last item killed
+            KeyPress::CTRL_W => {
+                // Kill the word behind point, using white space as a word boundary
+                match try!(edit_delete_prev_word(&mut s)) {
+                    Some(text) => kill_ring.kill(&text, false),
+                    None => (),
+                }
+            }
+            KeyPress::CTRL_Y => {
+                // retrieve (yank) last item killed
+                match kill_ring.yank() {
+                    Some(text) => try!(edit_yank(&mut s, text)),
+                    None => (),
+                }
+            }
+            KeyPress::ESC_Y => {
+                // yank-pop
+                match kill_ring.yank_pop() {
+                    Some((yank_size, text)) => try!(edit_yank_pop(&mut s, yank_size, text)),
+                    None => (),
+                }
+            }
             // TODO CTRL-_ // undo
-            KeyPress::ESC_SEQ_DELETE => try!(edit_delete(&mut s)),
-            KeyPress::ENTER => break, // Accept the line regardless of where the cursor is.
-            _ => try!(edit_insert(&mut s, ch)), // Insert the character typed.
+            KeyPress::ESC_SEQ_DELETE => {
+                kill_ring.reset();
+                try!(edit_delete(&mut s))
+            }
+            KeyPress::ENTER => {
+                kill_ring.reset();
+                // Accept the line regardless of where the cursor is.
+                break;
+            }
+            _ => {
+                kill_ring.reset();
+                // Insert the character typed.
+                try!(edit_insert(&mut s, ch))
+            }
         }
     }
     Ok(s.buf)
@@ -763,10 +870,11 @@ fn readline_edit(prompt: &str,
 /// method and disable raw mode
 fn readline_raw(prompt: &str,
                 history: &mut History,
-                completer: Option<&Completer>)
+                completer: Option<&Completer>,
+                kill_ring: &mut KillRing)
                 -> Result<String> {
     let original_termios = try!(enable_raw_mode());
-    let user_input = readline_edit(prompt, history, completer);
+    let user_input = readline_edit(prompt, history, completer, kill_ring);
     try!(disable_raw_mode(original_termios));
     println!("");
     user_input
@@ -788,6 +896,7 @@ pub struct Editor<'completer> {
     // cols: usize, // Number of columns in terminal
     history: History,
     completer: Option<&'completer Completer>,
+    kill_ring: KillRing,
 }
 
 impl<'completer> Editor<'completer> {
@@ -800,6 +909,7 @@ impl<'completer> Editor<'completer> {
             stdin_isatty: is_a_tty(),
             history: History::new(),
             completer: None,
+            kill_ring: KillRing::new(60),
         }
     }
 
@@ -815,7 +925,10 @@ impl<'completer> Editor<'completer> {
             // Not a tty: read from file / pipe.
             readline_direct()
         } else {
-            readline_raw(prompt, &mut self.history, self.completer)
+            readline_raw(prompt,
+                         &mut self.history,
+                         self.completer,
+                         &mut self.kill_ring)
         }
     }
 
@@ -941,14 +1054,16 @@ mod test {
     fn kill() {
         let mut out = ::std::io::sink();
         let mut s = init_state(&mut out, "αßγδε", 6, 80);
-        super::edit_kill_line(&mut s).unwrap();
+        let text = super::edit_kill_line(&mut s).unwrap();
         assert_eq!("αßγ", s.buf);
         assert_eq!(6, s.pos);
+        assert_eq!(Some("δε".to_string()), text);
 
         s.pos = 4;
-        super::edit_discard_line(&mut s).unwrap();
+        let text = super::edit_discard_line(&mut s).unwrap();
         assert_eq!("γ", s.buf);
         assert_eq!(0, s.pos);
+        assert_eq!(Some("αß".to_string()), text);
     }
 
     #[test]
@@ -970,9 +1085,10 @@ mod test {
     fn delete_prev_word() {
         let mut out = ::std::io::sink();
         let mut s = init_state(&mut out, "a ß  c", 6, 80);
-        super::edit_delete_prev_word(&mut s).unwrap();
+        let text = super::edit_delete_prev_word(&mut s).unwrap();
         assert_eq!("a c", s.buf);
         assert_eq!(2, s.pos);
+        assert_eq!(Some("ß  ".to_string()), text);
     }
 
     #[test]
-- 
GitLab