diff --git a/README.md b/README.md index 85eeb6d75fd3176f7b1e79b7dba4f94240d1cd23..88e8eb368fce3c38320bf14625552405565e3ccd 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,7 @@ Meta-Y | See Ctrl-Y Meta-BackSpace | Kill from the start of the current word, or, if between words, to the start of the previous word [Readline Emacs Editing Mode Cheat Sheet](http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf) +[Terminal codes (ANSI/VT100)](http://wiki.bash-hackers.org/scripting/terminalcodes) ## ToDo diff --git a/src/config.rs b/src/config.rs index 2708527e8d12a01f1bb010ba8b720ddb1bbd39bd..2585c3e33ac178a283c4bf85428367cb777165a3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,6 +13,7 @@ pub struct Config { completion_prompt_limit: usize, /// Duration (milliseconds) Rustyline will wait for a character when reading an ambiguous key sequence. keyseq_timeout: i32, + edit_mode: EditMode, } impl Config { @@ -48,6 +49,10 @@ impl Config { pub fn keyseq_timeout(&self) -> i32 { self.keyseq_timeout } + + pub fn edit_mode(&self) -> EditMode { + self.edit_mode + } } impl Default for Config { @@ -59,6 +64,7 @@ impl Default for Config { completion_type: CompletionType::Circular, // TODO Validate completion_prompt_limit: 100, keyseq_timeout: 500, + edit_mode: EditMode::Emacs, } } } @@ -79,6 +85,12 @@ pub enum CompletionType { List, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EditMode { + Emacs, + Vi, +} + #[derive(Debug)] pub struct Builder { p: Config, @@ -130,6 +142,11 @@ impl Builder { self } + pub fn edit_mode(mut self, edit_mode: EditMode) -> Builder { + self.p.edit_mode = edit_mode; + self + } + pub fn build(self) -> Config { self.p } diff --git a/src/keymap.rs b/src/keymap.rs new file mode 100644 index 0000000000000000000000000000000000000000..5acf4c1b1907786f9765fb7707e3cce9eb318bcf --- /dev/null +++ b/src/keymap.rs @@ -0,0 +1,338 @@ +use super::Config; +use super::EditMode; +use super::KeyPress; +use super::RawReader; +use super::Result; + +//#[derive(Clone)] +pub enum Cmd { + Abort, // Miscellaneous Command + AcceptLine, // Command For History + BackwardChar, // Command For Moving + BackwardDeleteChar, // Command For Text + BackwardKillWord, // Command For Killing + BackwardWord, // Command For Moving + BeginningOfHistory, // Command For History + BeginningOfLine, // Command For Moving + CapitalizeWord, // Command For Text + CharacterSearch(bool), // Miscellaneous Command (TODO Move right to the next occurance of c) + CharacterSearchBackward(bool), /* Miscellaneous Command (TODO Move left to the previous occurance of c) */ + ClearScreen, // Command For Moving + Complete, // Command For Completion + DeleteChar, // Command For Text + DowncaseWord, // Command For Text + EndOfFile, // Command For Text + EndOfHistory, // Command For History + EndOfLine, // Command For Moving + ForwardChar, // Command For Moving + ForwardSearchHistory, // Command For History + ForwardWord, // Command For Moving + KillLine, // Command For Killing + KillWholeLine, // Command For Killing (TODO Delete current line) + KillWord, // Command For Killing + NextHistory, // Command For History + Noop, + PreviousHistory, // Command For History + QuotedInsert, // Command For Text + Replace, // TODO DeleteChar + SelfInsert + ReverseSearchHistory, // Command For History + SelfInsert, // Command For Text + TransposeChars, // Command For Text + TransposeWords, // Command For Text + Unknown, + UnixLikeDiscard, // Command For Killing + UnixWordRubout, // Command For Killing + UpcaseWord, // Command For Text + Yank, // Command For Killing + YankPop, // Command For Killing +} + +// TODO numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7 +pub struct EditState { + mode: EditMode, + // TODO Validate Vi Command, Insert, Visual mode + insert: bool, // vi only ? +} + +impl EditState { + pub fn new(config: &Config) -> EditState { + EditState { + mode: config.edit_mode(), + insert: true, + } + } + + pub fn next_cmd<R: RawReader>(&mut self, + rdr: &mut R, + config: &Config) + -> Result<(KeyPress, Cmd)> { + match self.mode { + EditMode::Emacs => self.emacs(rdr, config), + EditMode::Vi if self.insert => self.vi_insert(rdr, config), + EditMode::Vi => self.vi_command(rdr, config), + } + } + + fn emacs<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<(KeyPress, Cmd)> { + let key = try!(rdr.next_key(config.keyseq_timeout())); + let cmd = match key { + KeyPress::Char(_) => Cmd::SelfInsert, + 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::Delete => Cmd::DeleteChar, + KeyPress::Ctrl('E') => Cmd::EndOfLine, + KeyPress::End => Cmd::EndOfLine, + KeyPress::Ctrl('F') => Cmd::ForwardChar, + KeyPress::Right => Cmd::ForwardChar, + KeyPress::Ctrl('G') => Cmd::Abort, + KeyPress::Ctrl('H') => Cmd::BackwardDeleteChar, + KeyPress::Backspace => Cmd::BackwardDeleteChar, + KeyPress::Tab => Cmd::Complete, + KeyPress::Ctrl('J') => Cmd::AcceptLine, + KeyPress::Enter => Cmd::AcceptLine, + KeyPress::Ctrl('K') => Cmd::KillLine, + KeyPress::Ctrl('L') => Cmd::ClearScreen, + KeyPress::Ctrl('N') => Cmd::NextHistory, + KeyPress::Down => Cmd::NextHistory, + KeyPress::Ctrl('P') => Cmd::PreviousHistory, + KeyPress::Up => Cmd::PreviousHistory, + KeyPress::Ctrl('Q') => Cmd::QuotedInsert, // most terminals override Ctrl+Q to resume execution + KeyPress::Ctrl('R') => Cmd::ReverseSearchHistory, + KeyPress::Ctrl('S') => Cmd::ForwardSearchHistory, // most terminals override Ctrl+S to suspend execution + KeyPress::Ctrl('T') => Cmd::TransposeChars, + KeyPress::Ctrl('U') => Cmd::UnixLikeDiscard, + KeyPress::Ctrl('V') => Cmd::QuotedInsert, + KeyPress::Ctrl('W') => Cmd::UnixWordRubout, + KeyPress::Ctrl('Y') => Cmd::Yank, + KeyPress::Meta('\x08') => Cmd::BackwardKillWord, + KeyPress::Meta('\x7f') => Cmd::BackwardKillWord, + // KeyPress::Meta('-') => { // digit-argument + // } + // KeyPress::Meta('0'...'9') => { // digit-argument + // } + KeyPress::Meta('<') => Cmd::BeginningOfHistory, + KeyPress::Meta('>') => Cmd::EndOfHistory, + KeyPress::Meta('B') => Cmd::BackwardWord, + KeyPress::Meta('C') => Cmd::CapitalizeWord, + KeyPress::Meta('D') => Cmd::KillWord, + KeyPress::Meta('F') => Cmd::ForwardWord, + KeyPress::Meta('L') => Cmd::DowncaseWord, + KeyPress::Meta('T') => Cmd::TransposeWords, + KeyPress::Meta('U') => Cmd::UpcaseWord, + KeyPress::Meta('Y') => Cmd::YankPop, + _ => Cmd::Unknown, + }; + Ok((key, cmd)) + } + + fn vi_command<R: RawReader>(&mut self, + rdr: &mut R, + config: &Config) + -> Result<(KeyPress, Cmd)> { + let key = try!(rdr.next_key(config.keyseq_timeout())); + let cmd = match key { + KeyPress::Char('$') => Cmd::EndOfLine, + // TODO KeyPress::Char('%') => Cmd::???, Move to the corresponding opening/closing bracket + KeyPress::Char('0') => Cmd::BeginningOfLine, // vi-zero: Vi move to the beginning of line. + // KeyPress::Char('1'...'9') => Cmd::???, // vi-arg-digit + KeyPress::Char('^') => Cmd::BeginningOfLine, // TODO Move to the first non-blank character of line. + KeyPress::Char('a') => { + // vi-append-mode: Vi enter insert mode after the cursor. + self.insert = true; + Cmd::ForwardChar + } + KeyPress::Char('A') => { + // vi-append-eol: Vi enter insert mode at end of line. + self.insert = true; + Cmd::EndOfLine + } + KeyPress::Char('b') => Cmd::BackwardWord, + // TODO KeyPress::Char('B') => Cmd::???, Move one non-blank word left. + KeyPress::Char('c') => { + self.insert = true; + let mvt = try!(rdr.next_key(config.keyseq_timeout())); + match mvt { + KeyPress::Char('$') => Cmd::KillLine, // vi-change-to-eol: Vi change to end of line. + KeyPress::Char('0') => Cmd::UnixLikeDiscard, + KeyPress::Char('c') => Cmd::KillWholeLine, + // TODO KeyPress::Char('f') => ???, + // TODO KeyPress::Char('F') => ???, + KeyPress::Char('h') => Cmd::BackwardDeleteChar, + KeyPress::Char('l') => Cmd::DeleteChar, + KeyPress::Char(' ') => Cmd::DeleteChar, + // TODO KeyPress::Char('t') => ???, + // TODO KeyPress::Char('T') => ???, + KeyPress::Char('w') => Cmd::KillWord, + _ => Cmd::Unknown, + } + } + KeyPress::Char('C') => { + self.insert = true; + Cmd::KillLine + } + KeyPress::Char('d') => { + let mvt = try!(rdr.next_key(config.keyseq_timeout())); + match mvt { + KeyPress::Char('$') => Cmd::KillLine, + KeyPress::Char('0') => Cmd::UnixLikeDiscard, // vi-kill-line-prev: Vi cut from beginning of line to cursor. + KeyPress::Char('d') => Cmd::KillWholeLine, + // TODO KeyPress::Char('f') => ???, + // TODO KeyPress::Char('F') => ???, + KeyPress::Char('h') => Cmd::BackwardDeleteChar, // vi-delete-prev-char: Vi move to previous character (backspace). + KeyPress::Char('l') => Cmd::DeleteChar, + KeyPress::Char(' ') => Cmd::DeleteChar, + // TODO KeyPress::Char('t') => ???, + // TODO KeyPress::Char('T') => ???, + KeyPress::Char('w') => Cmd::KillWord, + _ => Cmd::Unknown, + } + } + KeyPress::Char('D') => Cmd::KillLine, + // TODO KeyPress::Char('e') => Cmd::???, vi-to-end-word: Vi move to the end of the current word. Move to the end of the current word. + // TODO KeyPress::Char('E') => Cmd::???, vi-end-word: Vi move to the end of the current space delimited word. Move to the end of the current non-blank word. + KeyPress::Char('i') => { + // vi-insert: Vi enter insert mode. + self.insert = true; + Cmd::Noop + } + KeyPress::Char('I') => { + // vi-insert-at-bol: Vi enter insert mode at the beginning of line. + self.insert = true; + Cmd::BeginningOfLine + } + KeyPress::Char('f') => { + // vi-next-char: Vi move to the character specified next. + let ch = try!(rdr.next_key(config.keyseq_timeout())); + match ch { + KeyPress::Char(_) => return Ok((ch, Cmd::CharacterSearch(false))), + _ => Cmd::Unknown, + } + } + KeyPress::Char('F') => { + // vi-prev-char: Vi move to the character specified previous. + let ch = try!(rdr.next_key(config.keyseq_timeout())); + match ch { + KeyPress::Char(_) => return Ok((ch, Cmd::CharacterSearchBackward(false))), + _ => Cmd::Unknown, + } + } + // TODO KeyPress::Char('G') => Cmd::???, Move to the history line n + KeyPress::Char('p') => Cmd::Yank, // vi-paste-next: Vi paste previous deletion to the right of the cursor. + KeyPress::Char('P') => Cmd::Yank, // vi-paste-prev: Vi paste previous deletion to the left of the cursor. TODO Insert the yanked text before the cursor. + KeyPress::Char('r') => { + // 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::Esc => Cmd::Noop, + _ => Cmd::Unknown, + } + } + // TODO KeyPress::Char('R') => Cmd::???, vi-replace-mode: Vi enter replace mode. Replaces characters under the cursor. (overwrite-mode) + KeyPress::Char('s') => { + // vi-substitute-char: Vi replace character under the cursor and enter insert mode. + self.insert = true; + Cmd::DeleteChar + } + KeyPress::Char('S') => { + // vi-substitute-line: Vi substitute entire line. + self.insert = true; + Cmd::KillWholeLine + } + KeyPress::Char('t') => { + // vi-to-next-char: Vi move up to the character specified next. + let ch = try!(rdr.next_key(config.keyseq_timeout())); + match ch { + KeyPress::Char(_) => return Ok((ch, Cmd::CharacterSearchBackward(true))), + _ => Cmd::Unknown, + } + } + KeyPress::Char('T') => { + // vi-to-prev-char: Vi move up to the character specified previous. + let ch = try!(rdr.next_key(config.keyseq_timeout())); + match ch { + KeyPress::Char(_) => return Ok((ch, Cmd::CharacterSearch(true))), + _ => Cmd::Unknown, + } + } + // KeyPress::Char('U') => Cmd::???, // revert-line + KeyPress::Char('w') => Cmd::ForwardWord, // vi-next-word: Vi move to the next word. + // TODO KeyPress::Char('W') => Cmd::???, // vi-next-space-word: Vi move to the next space delimited word. Move one non-blank word right. + KeyPress::Char('x') => Cmd::DeleteChar, // vi-delete: TODO move backward if eol + KeyPress::Char('X') => Cmd::BackwardDeleteChar, + KeyPress::Home => Cmd::BeginningOfLine, + KeyPress::Char('h') => Cmd::BackwardChar, + KeyPress::Left => Cmd::BackwardChar, + KeyPress::Ctrl('D') => Cmd::EndOfFile, + KeyPress::Delete => Cmd::DeleteChar, + KeyPress::End => Cmd::EndOfLine, + KeyPress::Ctrl('G') => Cmd::Abort, + KeyPress::Ctrl('H') => Cmd::BackwardChar, + KeyPress::Char('l') => Cmd::ForwardChar, + KeyPress::Char(' ') => Cmd::ForwardChar, + KeyPress::Right => Cmd::ForwardChar, + KeyPress::Ctrl('L') => Cmd::ClearScreen, + KeyPress::Ctrl('J') => Cmd::AcceptLine, + KeyPress::Enter => Cmd::AcceptLine, + KeyPress::Char('+') => Cmd::NextHistory, + KeyPress::Char('j') => Cmd::NextHistory, + KeyPress::Ctrl('N') => Cmd::NextHistory, + KeyPress::Down => Cmd::NextHistory, + KeyPress::Char('-') => Cmd::PreviousHistory, + KeyPress::Char('k') => Cmd::PreviousHistory, + KeyPress::Ctrl('P') => Cmd::PreviousHistory, + 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('T') => Cmd::TransposeChars, + KeyPress::Ctrl('U') => Cmd::UnixLikeDiscard, + KeyPress::Ctrl('V') => Cmd::QuotedInsert, + KeyPress::Ctrl('W') => Cmd::UnixWordRubout, + KeyPress::Ctrl('Y') => Cmd::Yank, + KeyPress::Esc => Cmd::Noop, + _ => Cmd::Unknown, + }; + Ok((key, cmd)) + } + + fn vi_insert<R: RawReader>(&mut self, rdr: &mut R, config: &Config) -> Result<(KeyPress, Cmd)> { + let key = try!(rdr.next_key(config.keyseq_timeout())); + let cmd = match key { + KeyPress::Char(_) => Cmd::SelfInsert, + KeyPress::Home => Cmd::BeginningOfLine, + KeyPress::Left => Cmd::BackwardChar, + KeyPress::Ctrl('D') => Cmd::EndOfFile, // vi-eof-maybe + KeyPress::Delete => Cmd::DeleteChar, + KeyPress::End => Cmd::EndOfLine, + KeyPress::Right => Cmd::ForwardChar, + KeyPress::Ctrl('H') => Cmd::BackwardDeleteChar, + KeyPress::Backspace => Cmd::BackwardDeleteChar, + KeyPress::Tab => Cmd::Complete, + KeyPress::Ctrl('J') => Cmd::AcceptLine, + KeyPress::Enter => Cmd::AcceptLine, + KeyPress::Down => Cmd::NextHistory, + KeyPress::Up => Cmd::PreviousHistory, + KeyPress::Ctrl('R') => Cmd::ReverseSearchHistory, + KeyPress::Ctrl('S') => Cmd::ForwardSearchHistory, + KeyPress::Ctrl('T') => Cmd::TransposeChars, + KeyPress::Ctrl('U') => Cmd::UnixLikeDiscard, + KeyPress::Ctrl('V') => Cmd::QuotedInsert, + KeyPress::Ctrl('W') => Cmd::UnixWordRubout, + KeyPress::Ctrl('Y') => Cmd::Yank, + KeyPress::Esc => { + // vi-movement-mode/vi-command-mode: Vi enter command mode (use alternative key bindings). + self.insert = false; + Cmd::Noop + } + _ => Cmd::Unknown, + }; + Ok((key, cmd)) + } +} diff --git a/src/lib.rs b/src/lib.rs index 59ae361f624b9ce02a0bc28acccbee65a2ac9765..ca2bb251931c84a80f7a20360bbc0603c6bf0505 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub mod completion; mod consts; pub mod error; pub mod history; +mod keymap; mod kill_ring; pub mod line_buffer; pub mod config; @@ -49,7 +50,7 @@ use consts::KeyPress; use history::{Direction, History}; use line_buffer::{LineBuffer, MAX_LINE, WordAction}; use kill_ring::{Mode, KillRing}; -pub use config::{CompletionType, Config, HistoryDuplicates}; +pub use config::{CompletionType, Config, EditMode, HistoryDuplicates}; /// The error type for I/O and Linux Syscalls (Errno) pub type Result<T> = result::Result<T, error::ReadlineError>; diff --git a/src/line_buffer.rs b/src/line_buffer.rs index abfe1856dc83b78175fbf3098d2e68a860601e6e..613fce1cd0148cb03177e591105de665067835c8 100644 --- a/src/line_buffer.rs +++ b/src/line_buffer.rs @@ -183,6 +183,11 @@ impl LineBuffer { } } + /// Replace a single character under the cursor (Vi mode) + pub fn replace_char(&mut self, ch: char) -> Option<bool> { + if self.delete() { self.insert(ch) } else { None } + } + /// Delete the character at the right of the cursor without altering the cursor /// position. Basically this is what happens with the "Delete" keyboard key. pub fn delete(&mut self) -> bool { @@ -206,6 +211,12 @@ impl LineBuffer { } } + /// Kill all characters on the current line. + pub fn kill_whole_line(&mut self) -> Option<String> { + self.move_home(); + self.kill_line() + } + /// Kill the text from point to the end of the line. pub fn kill_line(&mut self) -> Option<String> { if !self.buf.is_empty() && self.pos < self.buf.len() { diff --git a/src/tty/unix.rs b/src/tty/unix.rs index 4e7f5f18ee54f81984a372f66e7cfede08731571..b51cdba20805760794701f7896757b629a35fe50 100644 --- a/src/tty/unix.rs +++ b/src/tty/unix.rs @@ -152,6 +152,8 @@ impl PosixRawReader { // TODO ESC-R (r): Undo all changes made to this line. match seq1 { '\x08' => Ok(KeyPress::Meta('\x08')), // Backspace + '-' => return Ok(KeyPress::Meta('-')), + '0'...'9' => return Ok(KeyPress::Meta(seq1)), '<' => Ok(KeyPress::Meta('<')), '>' => Ok(KeyPress::Meta('>')), 'b' | 'B' => Ok(KeyPress::Meta('B')), diff --git a/src/tty/windows.rs b/src/tty/windows.rs index 6cbb0c3d90e0c10ad4bb14d69901745d3cedaec9..1ebf108d9dca2473f7e6833b482971bcf46f0d6b 100644 --- a/src/tty/windows.rs +++ b/src/tty/windows.rs @@ -151,6 +151,10 @@ impl RawReader for ConsoleRawReader { let c = try!(orc.unwrap()); if meta { match c { + '-' => return Ok(KeyPress::Meta('-')), + '0'...'9' => return Ok(KeyPress::Meta(c)), + '<' => Ok(KeyPress::Meta('<')), + '>' => Ok(KeyPress::Meta('>')), 'b' | 'B' => return Ok(KeyPress::Meta('B')), 'c' | 'C' => return Ok(KeyPress::Meta('C')), 'd' | 'D' => return Ok(KeyPress::Meta('D')),