From e56e5c9fb91992987ebef1513916a386dc14e6eb Mon Sep 17 00:00:00 2001
From: gwenn <gtreguier@gmail.com>
Date: Sun, 11 Dec 2016 17:15:21 +0100
Subject: [PATCH] Vi mode support (#94)

---
 src/keymap.rs |  69 ++++++++++------
 src/lib.rs    | 213 +++++++++++++++++++++++++-------------------------
 2 files changed, 152 insertions(+), 130 deletions(-)

diff --git a/src/keymap.rs b/src/keymap.rs
index e2a44e5e..7f2e7dbb 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -4,7 +4,7 @@ use super::KeyPress;
 use super::RawReader;
 use super::Result;
 
-//#[derive(Clone)]
+#[derive(Debug, Clone, PartialEq)]
 pub enum Cmd {
     Abort, // Miscellaneous Command
     AcceptLine, // Command For History
@@ -25,16 +25,18 @@ pub enum Cmd {
     ForwardChar, // Command For Moving
     ForwardSearchHistory, // Command For History
     ForwardWord(Word), // Command For Moving
+    Interrupt,
     KillLine, // Command For Killing
     KillWholeLine, // Command For Killing (TODO Delete current line)
     KillWord(Word), // Command For Killing
     NextHistory, // Command For History
-    Noop, // TODO
+    Noop,
     PreviousHistory, // Command For History
     QuotedInsert, // Command For Text
-    Replace, // TODO DeleteChar + SelfInsert
+    Replace(char), // TODO DeleteChar + SelfInsert
     ReverseSearchHistory, // Command For History
-    SelfInsert, // Command For Text
+    SelfInsert(char), // Command For Text
+    Suspend,
     TransposeChars, // Command For Text
     TransposeWords, // Command For Text
     Unknown,
@@ -48,6 +50,7 @@ pub enum Cmd {
     YankPop, // Command For Killing
 }
 
+#[derive(Debug, Clone, PartialEq)]
 pub enum Word {
     // non-blanks characters
     BigWord,
@@ -57,6 +60,7 @@ pub enum Word {
     ViWord,
 }
 
+#[derive(Debug, Clone, PartialEq)]
 pub enum CharSearch {
     Forward(char),
     // until
@@ -81,10 +85,11 @@ impl EditState {
         }
     }
 
-    pub fn next_cmd<R: RawReader>(&mut self,
-                                  rdr: &mut R,
-                                  config: &Config)
-                                  -> Result<(KeyPress, Cmd)> {
+    pub fn is_emacs_mode(&self) -> bool {
+        self.mode == EditMode::Emacs
+    }
+
+    pub fn next_cmd<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<Cmd> {
         match self.mode {
             EditMode::Emacs => self.emacs(rdr, config),
             EditMode::Vi if self.insert => self.vi_insert(rdr, config),
@@ -92,17 +97,18 @@ impl EditState {
         }
     }
 
-    fn emacs<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<(KeyPress, Cmd)> {
+    fn emacs<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<Cmd> {
         let key = try!(rdr.next_key(config.keyseq_timeout()));
         let cmd = match key {
-            KeyPress::Char(_) => Cmd::SelfInsert,
+            KeyPress::Char(c) => Cmd::SelfInsert(c),
             KeyPress::Esc => Cmd::Abort, // TODO Validate
             KeyPress::Ctrl('A') => Cmd::BeginningOfLine,
             KeyPress::Home => Cmd::BeginningOfLine,
             KeyPress::Ctrl('B') => Cmd::BackwardChar,
             KeyPress::Left => Cmd::BackwardChar,
-            // KeyPress::Ctrl('D') if s.line.is_empty() => Cmd::EndOfFile,
-            KeyPress::Ctrl('D') => Cmd::DeleteChar,
+            KeyPress::Ctrl('C') => Cmd::Interrupt,
+            KeyPress::Ctrl('D') => Cmd::EndOfFile,
+            // KeyPress::Ctrl('D') => Cmd::DeleteChar,
             KeyPress::Delete => Cmd::DeleteChar,
             KeyPress::Ctrl('E') => Cmd::EndOfLine,
             KeyPress::End => Cmd::EndOfLine,
@@ -128,6 +134,7 @@ impl EditState {
             KeyPress::Ctrl('V') => Cmd::QuotedInsert,
             KeyPress::Ctrl('W') => Cmd::KillWord(Word::BigWord),
             KeyPress::Ctrl('Y') => Cmd::Yank,
+            KeyPress::Ctrl('Z') => Cmd::Suspend,
             KeyPress::Meta('\x08') => Cmd::BackwardKillWord(Word::Word),
             KeyPress::Meta('\x7f') => Cmd::BackwardKillWord(Word::Word),
             // KeyPress::Meta('-') => { // digit-argument
@@ -144,15 +151,13 @@ impl EditState {
             KeyPress::Meta('T') => Cmd::TransposeWords,
             KeyPress::Meta('U') => Cmd::UpcaseWord,
             KeyPress::Meta('Y') => Cmd::YankPop,
+            KeyPress::UnknownEscSeq => Cmd::Noop,
             _ => Cmd::Unknown,
         };
-        Ok((key, cmd))
+        Ok(cmd)
     }
 
-    fn vi_command<R: RawReader>(&mut self,
-                                rdr: &mut R,
-                                config: &Config)
-                                -> Result<(KeyPress, Cmd)> {
+    fn vi_command<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<Cmd> {
         let key = try!(rdr.next_key(config.keyseq_timeout()));
         let cmd = match key {
             KeyPress::Char('$') => Cmd::EndOfLine,
@@ -211,7 +216,7 @@ impl EditState {
                 // vi-replace-char: Vi replace character under the cursor with the next character typed.
                 let ch = try!(rdr.next_key(config.keyseq_timeout()));
                 match ch {
-                    KeyPress::Char(_) => return Ok((ch, Cmd::Replace)),
+                    KeyPress::Char(c) => Cmd::Replace(c),
                     KeyPress::Esc => Cmd::Noop,
                     _ => Cmd::Unknown,
                 }
@@ -238,6 +243,7 @@ impl EditState {
             KeyPress::Ctrl('H') => Cmd::BackwardChar,
             KeyPress::Backspace => Cmd::BackwardChar, // TODO Validate
             KeyPress::Left => Cmd::BackwardChar,
+            KeyPress::Ctrl('C') => Cmd::Interrupt,
             KeyPress::Ctrl('D') => Cmd::EndOfFile,
             KeyPress::Delete => Cmd::DeleteChar,
             KeyPress::Ctrl('G') => Cmd::Abort,
@@ -257,25 +263,34 @@ impl EditState {
             KeyPress::Up => Cmd::PreviousHistory,
             KeyPress::Ctrl('K') => Cmd::KillLine,
             KeyPress::Ctrl('Q') => Cmd::QuotedInsert, // most terminals override Ctrl+Q to resume execution
-            KeyPress::Ctrl('R') => Cmd::ReverseSearchHistory,
-            KeyPress::Ctrl('S') => Cmd::ForwardSearchHistory,
+            KeyPress::Ctrl('R') => {
+                self.insert = true; // TODO Validate
+                Cmd::ReverseSearchHistory
+            }
+            KeyPress::Ctrl('S') => {
+                self.insert = true; // TODO Validate
+                Cmd::ForwardSearchHistory
+            }
             KeyPress::Ctrl('T') => Cmd::TransposeChars,
             KeyPress::Ctrl('U') => Cmd::UnixLikeDiscard,
             KeyPress::Ctrl('V') => Cmd::QuotedInsert,
             KeyPress::Ctrl('W') => Cmd::KillWord(Word::BigWord),
             KeyPress::Ctrl('Y') => Cmd::Yank,
+            KeyPress::Ctrl('Z') => Cmd::Suspend,
             KeyPress::Esc => Cmd::Noop,
+            KeyPress::UnknownEscSeq => Cmd::Noop,
             _ => Cmd::Unknown,
         };
-        Ok((key, cmd))
+        Ok(cmd)
     }
 
-    fn vi_insert<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<(KeyPress, Cmd)> {
+    fn vi_insert<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<Cmd> {
         let key = try!(rdr.next_key(config.keyseq_timeout()));
         let cmd = match key {
-            KeyPress::Char(_) => Cmd::SelfInsert,
+            KeyPress::Char(c) => Cmd::SelfInsert(c),
             KeyPress::Home => Cmd::BeginningOfLine,
             KeyPress::Left => Cmd::BackwardChar,
+            KeyPress::Ctrl('C') => Cmd::Interrupt,
             KeyPress::Ctrl('D') => Cmd::EndOfFile, // vi-eof-maybe
             KeyPress::Delete => Cmd::DeleteChar,
             KeyPress::End => Cmd::EndOfLine,
@@ -294,14 +309,16 @@ impl EditState {
             KeyPress::Ctrl('V') => Cmd::QuotedInsert,
             KeyPress::Ctrl('W') => Cmd::KillWord(Word::BigWord),
             KeyPress::Ctrl('Y') => Cmd::Yank,
+            KeyPress::Ctrl('Z') => Cmd::Suspend,
             KeyPress::Esc => {
                 // vi-movement-mode/vi-command-mode: Vi enter command mode (use alternative key bindings).
                 self.insert = false;
                 Cmd::Noop
             }
+            KeyPress::UnknownEscSeq => Cmd::Noop,
             _ => Cmd::Unknown,
         };
-        Ok((key, cmd))
+        Ok(cmd)
     }
 
     fn vi_delete_motion<R: RawReader>(&mut self,
@@ -310,12 +327,14 @@ impl EditState {
                                       key: KeyPress)
                                       -> Result<Cmd> {
         let mvt = try!(rdr.next_key(config.keyseq_timeout()));
+        if mvt == key {
+            return Ok(Cmd::KillWholeLine);
+        }
         Ok(match mvt {
             KeyPress::Char('$') => Cmd::KillLine, // vi-change-to-eol: Vi change to end of line.
             KeyPress::Char('0') => Cmd::UnixLikeDiscard, // vi-kill-line-prev: Vi cut from beginning of line to cursor.
             KeyPress::Char('b') => Cmd::BackwardKillWord(Word::ViWord),
             KeyPress::Char('B') => Cmd::BackwardKillWord(Word::BigWord),
-            x if x == key => Cmd::KillWholeLine,
             KeyPress::Char('e') => Cmd::KillWord(Word::ViWord),
             KeyPress::Char('E') => Cmd::KillWord(Word::BigWord),
             KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
diff --git a/src/lib.rs b/src/lib.rs
index ca2bb251..2f5a8011 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -49,6 +49,7 @@ use completion::{Completer, longest_common_prefix};
 use consts::KeyPress;
 use history::{Direction, History};
 use line_buffer::{LineBuffer, MAX_LINE, WordAction};
+use keymap::{CharSearch, Cmd, EditState, Word};
 use kill_ring::{Mode, KillRing};
 pub use config::{CompletionType, Config, EditMode, HistoryDuplicates};
 
@@ -68,6 +69,7 @@ struct State<'out, 'prompt> {
     snapshot: LineBuffer, // Current edited line before history browsing/completion
     term: Terminal, // terminal
     byte_buffer: [u8; 4],
+    edit_state: EditState,
 }
 
 #[derive(Copy, Clone, Debug, Default)]
@@ -79,6 +81,7 @@ struct Position {
 impl<'out, 'prompt> State<'out, 'prompt> {
     fn new(out: &'out mut Write,
            term: Terminal,
+           config: &Config,
            prompt: &'prompt str,
            history_index: usize)
            -> State<'out, 'prompt> {
@@ -97,6 +100,19 @@ impl<'out, 'prompt> State<'out, 'prompt> {
             snapshot: LineBuffer::with_capacity(capacity),
             term: term,
             byte_buffer: [0; 4],
+            edit_state: EditState::new(config),
+        }
+    }
+
+    fn next_cmd<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<Cmd> {
+        loop {
+            let rc = self.edit_state.next_cmd(rdr, config);
+            if rc.is_err() && self.term.sigwinch() {
+                self.update_columns();
+                try!(self.refresh_line());
+                continue;
+            }
+            return rc;
         }
     }
 
@@ -529,7 +545,7 @@ fn complete_line<R: RawReader>(rdr: &mut R,
                                s: &mut State,
                                completer: &Completer,
                                config: &Config)
-                               -> Result<Option<KeyPress>> {
+                               -> Result<Option<Cmd>> {
     // get a list of completions
     let (start, candidates) = try!(completer.complete(&s.line, s.line.pos()));
     // if no completions, we are done
@@ -539,7 +555,7 @@ fn complete_line<R: RawReader>(rdr: &mut R,
     } else if CompletionType::Circular == config.completion_type() {
         // Save the current edited line before to overwrite it
         s.backup();
-        let mut key;
+        let mut cmd;
         let mut i = 0;
         loop {
             // Show completion or original buffer
@@ -553,15 +569,15 @@ fn complete_line<R: RawReader>(rdr: &mut R,
                 s.snapshot();
             }
 
-            key = try!(rdr.next_key(config.keyseq_timeout()));
-            match key {
-                KeyPress::Tab => {
+            cmd = try!(s.next_cmd(rdr, config));
+            match cmd {
+                Cmd::Complete => {
                     i = (i + 1) % (candidates.len() + 1); // Circular
                     if i == candidates.len() {
                         try!(beep());
                     }
                 }
-                KeyPress::Esc => {
+                Cmd::Abort => {
                     // Re-show original buffer
                     s.snapshot();
                     if i < candidates.len() {
@@ -577,7 +593,7 @@ fn complete_line<R: RawReader>(rdr: &mut R,
                 }
             }
         }
-        Ok(Some(key))
+        Ok(Some(cmd))
     } else if CompletionType::List == config.completion_type() {
         // beep if ambiguous
         if candidates.len() > 1 {
@@ -592,10 +608,10 @@ fn complete_line<R: RawReader>(rdr: &mut R,
             }
         }
         // we can't complete any further, wait for second tab
-        let mut key = try!(rdr.next_key(config.keyseq_timeout()));
+        let mut cmd = try!(s.next_cmd(rdr, config));
         // if any character other than tab, pass it to the main loop
-        if key != KeyPress::Tab {
-            return Ok(Some(key));
+        if cmd != Cmd::Complete {
+            return Ok(Some(cmd));
         }
         // move cursor to EOL to avoid overwriting the command line
         let save_pos = s.line.pos();
@@ -607,14 +623,14 @@ fn complete_line<R: RawReader>(rdr: &mut R,
             let msg = format!("\nDisplay all {} possibilities? (y or n)", candidates.len());
             try!(write_and_flush(s.out, msg.as_bytes()));
             s.old_rows += 1;
-            while key != KeyPress::Char('y') && key != KeyPress::Char('Y') &&
-                  key != KeyPress::Char('n') && key != KeyPress::Char('N') &&
-                  key != KeyPress::Backspace {
-                key = try!(rdr.next_key(config.keyseq_timeout()));
+            while cmd != Cmd::SelfInsert('y') && cmd != Cmd::SelfInsert('Y') &&
+                  cmd != Cmd::SelfInsert('n') && cmd != Cmd::SelfInsert('N') &&
+                  cmd != Cmd::BackwardDeleteChar {
+                cmd = try!(s.next_cmd(rdr, config));
             }
-            show_completions = match key {
-                KeyPress::Char('y') |
-                KeyPress::Char('Y') => true,
+            show_completions = match cmd {
+                Cmd::SelfInsert('y') |
+                Cmd::SelfInsert('Y') => true,
                 _ => false,
             };
         }
@@ -633,7 +649,7 @@ fn page_completions<R: RawReader>(rdr: &mut R,
                                   s: &mut State,
                                   config: &Config,
                                   candidates: &[String])
-                                  -> Result<Option<KeyPress>> {
+                                  -> Result<Option<Cmd>> {
     use std::cmp;
     use unicode_width::UnicodeWidthStr;
 
@@ -701,7 +717,7 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R,
                                             s: &mut State,
                                             history: &History,
                                             config: &Config)
-                                            -> Result<Option<KeyPress>> {
+                                            -> Result<Option<Cmd>> {
     if history.is_empty() {
         return Ok(None);
     }
@@ -713,7 +729,7 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R,
     let mut direction = Direction::Reverse;
     let mut success = true;
 
-    let mut key;
+    let mut cmd;
     // Display the reverse-i-search prompt and process chars
     loop {
         let prompt = if success {
@@ -723,17 +739,16 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R,
         };
         try!(s.refresh_prompt_and_line(&prompt));
 
-        key = try!(rdr.next_key(config.keyseq_timeout()));
-        if let KeyPress::Char(c) = key {
+        cmd = try!(s.next_cmd(rdr, config));
+        if let Cmd::SelfInsert(c) = cmd {
             search_buf.push(c);
         } else {
-            match key {
-                KeyPress::Ctrl('H') |
-                KeyPress::Backspace => {
+            match cmd {
+                Cmd::BackwardDeleteChar => {
                     search_buf.pop();
                     continue;
                 }
-                KeyPress::Ctrl('R') => {
+                Cmd::ReverseSearchHistory => {
                     direction = Direction::Reverse;
                     if history_idx > 0 {
                         history_idx -= 1;
@@ -742,7 +757,7 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R,
                         continue;
                     }
                 }
-                KeyPress::Ctrl('S') => {
+                Cmd::ForwardSearchHistory => {
                     direction = Direction::Forward;
                     if history_idx < history.len() - 1 {
                         history_idx += 1;
@@ -751,7 +766,7 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R,
                         continue;
                     }
                 }
-                KeyPress::Ctrl('G') => {
+                Cmd::Abort => {
                     // Restore current edited line (before search)
                     s.snapshot();
                     try!(s.refresh_line());
@@ -771,7 +786,7 @@ fn reverse_incremental_search<R: RawReader>(rdr: &mut R,
             _ => false,
         };
     }
-    Ok(Some(key))
+    Ok(Some(cmd))
 }
 
 /// Handles reading and editting the readline buffer.
@@ -789,6 +804,7 @@ fn readline_edit<C: Completer>(prompt: &str,
     editor.kill_ring.reset();
     let mut s = State::new(&mut stdout,
                            editor.term.clone(),
+                           &editor.config,
                            prompt,
                            editor.history.len());
     try!(s.refresh_line());
@@ -796,159 +812,135 @@ fn readline_edit<C: Completer>(prompt: &str,
     let mut rdr = try!(s.term.create_reader());
 
     loop {
-        let rk = rdr.next_key(editor.config.keyseq_timeout());
-        if rk.is_err() && s.term.sigwinch() {
-            s.update_columns();
-            try!(s.refresh_line());
-            continue;
-        }
-        let mut key = try!(rk);
-        if let KeyPress::Char(c) = key {
-            editor.kill_ring.reset();
-            try!(edit_insert(&mut s, c));
-            continue;
-        }
+        let rc = s.next_cmd(&mut rdr, &editor.config);
+        let mut cmd = try!(rc);
 
         // autocomplete
-        if key == KeyPress::Tab && completer.is_some() {
+        if cmd == Cmd::Complete && completer.is_some() {
             let next = try!(complete_line(&mut rdr, &mut s, completer.unwrap(), &editor.config));
             if next.is_some() {
                 editor.kill_ring.reset();
-                key = next.unwrap();
-                if let KeyPress::Char(c) = key {
-                    try!(edit_insert(&mut s, c));
-                    continue;
-                }
+                cmd = next.unwrap();
             } else {
                 continue;
             }
-        } else if key == KeyPress::Ctrl('R') {
+        }
+
+        if let Cmd::SelfInsert(c) = cmd {
+            editor.kill_ring.reset();
+            try!(edit_insert(&mut s, c));
+            continue;
+        }
+
+        if cmd == Cmd::ReverseSearchHistory {
             // Search history backward
             let next =
                 try!(reverse_incremental_search(&mut rdr, &mut s, &editor.history, &editor.config));
             if next.is_some() {
-                key = next.unwrap();
+                cmd = next.unwrap();
             } else {
                 continue;
             }
-        } else if key == KeyPress::UnknownEscSeq {
-            continue;
         }
 
-        match key {
-            KeyPress::Ctrl('A') |
-            KeyPress::Home => {
+        match cmd {
+            Cmd::BeginningOfLine => {
                 editor.kill_ring.reset();
                 // Move to the beginning of line.
                 try!(edit_move_home(&mut s))
             }
-            KeyPress::Ctrl('B') |
-            KeyPress::Left => {
+            Cmd::BackwardChar => {
                 editor.kill_ring.reset();
                 // Move back a character.
                 try!(edit_move_left(&mut s))
             }
-            KeyPress::Ctrl('C') => {
+            Cmd::DeleteChar => {
                 editor.kill_ring.reset();
-                return Err(error::ReadlineError::Interrupted);
+                // Delete (forward) one character at point.
+                try!(edit_delete(&mut s))
             }
-            KeyPress::Ctrl('D') => {
+            Cmd::EndOfFile => {
                 editor.kill_ring.reset();
-                if s.line.is_empty() {
+                if !s.edit_state.is_emacs_mode() || s.line.is_empty() {
                     return Err(error::ReadlineError::Eof);
                 } else {
-                    // Delete (forward) one character at point.
                     try!(edit_delete(&mut s))
                 }
             }
-            KeyPress::Ctrl('E') |
-            KeyPress::End => {
+            Cmd::EndOfLine => {
                 editor.kill_ring.reset();
                 // Move to the end of line.
                 try!(edit_move_end(&mut s))
             }
-            KeyPress::Ctrl('F') |
-            KeyPress::Right => {
+            Cmd::ForwardChar => {
                 editor.kill_ring.reset();
                 // Move forward a character.
                 try!(edit_move_right(&mut s))
             }
-            KeyPress::Ctrl('H') |
-            KeyPress::Backspace => {
+            Cmd::BackwardDeleteChar => {
                 editor.kill_ring.reset();
                 // Delete one character backward.
                 try!(edit_backspace(&mut s))
             }
-            KeyPress::Ctrl('K') => {
+            Cmd::KillLine => {
                 // Kill the text from point to the end of the line.
                 if let Some(text) = try!(edit_kill_line(&mut s)) {
                     editor.kill_ring.kill(&text, Mode::Append)
                 }
             }
-            KeyPress::Ctrl('L') => {
+            Cmd::ClearScreen => {
                 // Clear the screen leaving the current line at the top of the screen.
                 try!(s.term.clear_screen(&mut s.out));
                 try!(s.refresh_line())
             }
-            KeyPress::Ctrl('N') |
-            KeyPress::Down => {
+            Cmd::NextHistory => {
                 editor.kill_ring.reset();
                 // Fetch the next command from the history list.
                 try!(edit_history_next(&mut s, &editor.history, false))
             }
-            KeyPress::Ctrl('P') |
-            KeyPress::Up => {
+            Cmd::PreviousHistory => {
                 editor.kill_ring.reset();
                 // Fetch the previous command from the history list.
                 try!(edit_history_next(&mut s, &editor.history, true))
             }
-            KeyPress::Ctrl('T') => {
+            Cmd::TransposeChars => {
                 editor.kill_ring.reset();
                 // Exchange the char before cursor with the character at cursor.
                 try!(edit_transpose_chars(&mut s))
             }
-            KeyPress::Ctrl('U') => {
+            Cmd::UnixLikeDiscard => {
                 // Kill backward from point to the beginning of the line.
                 if let Some(text) = try!(edit_discard_line(&mut s)) {
                     editor.kill_ring.kill(&text, Mode::Prepend)
                 }
             }
             #[cfg(unix)]
-            KeyPress::Ctrl('V') => {
+            Cmd::QuotedInsert => {
                 // Quoted insert
                 editor.kill_ring.reset();
                 let c = try!(rdr.next_char());
                 try!(edit_insert(&mut s, c)) // FIXME
             }
-            KeyPress::Ctrl('W') => {
+            Cmd::KillWord(Word::BigWord) => {
                 // Kill the word behind point, using white space as a word boundary
                 if let Some(text) = try!(edit_delete_prev_word(&mut s, char::is_whitespace)) {
                     editor.kill_ring.kill(&text, Mode::Prepend)
                 }
             }
-            KeyPress::Ctrl('Y') => {
+            Cmd::Yank => {
                 // retrieve (yank) last item killed
                 if let Some(text) = editor.kill_ring.yank() {
                     try!(edit_yank(&mut s, text))
                 }
             }
-            #[cfg(unix)]
-            KeyPress::Ctrl('Z') => {
-                try!(original_mode.disable_raw_mode());
-                try!(tty::suspend());
-                try!(s.term.enable_raw_mode()); // TODO original_mode may have changed
-                try!(s.refresh_line())
-            }
             // TODO CTRL-_ // undo
-            KeyPress::Enter |
-            KeyPress::Ctrl('J') => {
+            Cmd::AcceptLine => {
                 // Accept the line regardless of where the cursor is.
                 editor.kill_ring.reset();
                 try!(edit_move_end(&mut s));
                 break;
             }
-            KeyPress::Meta('\x08') |
-            KeyPress::Meta('\x7f') => {
+            Cmd::BackwardKillWord(Word::Word) => {
                 // kill one word backward
                 // Kill from the cursor to the start of the current word, or, if between words, to the start of the previous word.
                 if let Some(text) = try!(edit_delete_prev_word(&mut s,
@@ -956,62 +948,71 @@ fn readline_edit<C: Completer>(prompt: &str,
                     editor.kill_ring.kill(&text, Mode::Prepend)
                 }
             }
-            KeyPress::Meta('<') => {
+            Cmd::BeginningOfHistory => {
                 // move to first entry in history
                 editor.kill_ring.reset();
                 try!(edit_history(&mut s, &editor.history, true))
             }
-            KeyPress::Meta('>') => {
+            Cmd::EndOfHistory => {
                 // move to last entry in history
                 editor.kill_ring.reset();
                 try!(edit_history(&mut s, &editor.history, false))
             }
-            KeyPress::Meta('B') => {
+            Cmd::BackwardWord(Word::Word) => {
                 // move backwards one word
                 editor.kill_ring.reset();
                 try!(edit_move_to_prev_word(&mut s))
             }
-            KeyPress::Meta('C') => {
+            Cmd::CapitalizeWord => {
                 // capitalize word after point
                 editor.kill_ring.reset();
                 try!(edit_word(&mut s, WordAction::CAPITALIZE))
             }
-            KeyPress::Meta('D') => {
+            Cmd::KillWord(Word::Word) => {
                 // kill one word forward
                 if let Some(text) = try!(edit_delete_word(&mut s)) {
                     editor.kill_ring.kill(&text, Mode::Append)
                 }
             }
-            KeyPress::Meta('F') => {
+            Cmd::ForwardWord(Word::Word) => {
                 // move forwards one word
                 editor.kill_ring.reset();
                 try!(edit_move_to_next_word(&mut s))
             }
-            KeyPress::Meta('L') => {
+            Cmd::DowncaseWord => {
                 // lowercase word after point
                 editor.kill_ring.reset();
                 try!(edit_word(&mut s, WordAction::LOWERCASE))
             }
-            KeyPress::Meta('T') => {
+            Cmd::TransposeWords => {
                 // transpose words
                 editor.kill_ring.reset();
                 try!(edit_transpose_words(&mut s))
             }
-            KeyPress::Meta('U') => {
+            Cmd::UpcaseWord => {
                 // uppercase word after point
                 editor.kill_ring.reset();
                 try!(edit_word(&mut s, WordAction::UPPERCASE))
             }
-            KeyPress::Meta('Y') => {
+            Cmd::YankPop => {
                 // yank-pop
                 if let Some((yank_size, text)) = editor.kill_ring.yank_pop() {
                     try!(edit_yank_pop(&mut s, yank_size, text))
                 }
             }
-            KeyPress::Delete => {
+            Cmd::Interrupt => {
                 editor.kill_ring.reset();
-                try!(edit_delete(&mut s))
+                return Err(error::ReadlineError::Interrupted);
+            }
+            #[cfg(unix)]
+            Cmd::Suspend => {
+                try!(original_mode.disable_raw_mode());
+                try!(tty::suspend());
+                try!(s.term.enable_raw_mode()); // TODO original_mode may have changed
+                try!(s.refresh_line());
+                continue;
             }
+            Cmd::Noop => {}
             _ => {
                 editor.kill_ring.reset();
                 // Ignore the character typed.
@@ -1180,8 +1181,8 @@ mod test {
     use completion::Completer;
     use config::Config;
     use consts::KeyPress;
-    use {Position, State};
-    use super::{Editor, Result};
+    use keymap::{Cmd, EditState};
+    use super::{Editor, Position, Result, State};
     use tty::{Terminal, Term};
 
     fn init_state<'out>(out: &'out mut Write,
@@ -1190,6 +1191,7 @@ mod test {
                         cols: usize)
                         -> State<'out, 'static> {
         let term = Terminal::new();
+        let config = Config::default();
         State {
             out: out,
             prompt: "",
@@ -1202,6 +1204,7 @@ mod test {
             snapshot: LineBuffer::with_capacity(100),
             term: term,
             byte_buffer: [0; 4],
+            edit_state : EditState::new(&config),
         }
     }
 
@@ -1263,8 +1266,8 @@ mod test {
         let keys = &[KeyPress::Enter];
         let mut rdr = keys.iter();
         let completer = SimpleCompleter;
-        let key = super::complete_line(&mut rdr, &mut s, &completer, &Config::default()).unwrap();
-        assert_eq!(Some(KeyPress::Enter), key);
+        let cmd = super::complete_line(&mut rdr, &mut s, &completer, &Config::default()).unwrap();
+        assert_eq!(Some(Cmd::AcceptLine), cmd);
         assert_eq!("rust", s.line.as_str());
         assert_eq!(4, s.line.pos());
     }
-- 
GitLab