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