blob: 877a12dd6a85887886d39e5ea7609f1c3652e1fa [file] [log] [blame]
//! Bindings from keys to command for Emacs and Vi modes
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use super::Result;
use config::Config;
use config::EditMode;
use consts::KeyPress;
use tty::RawReader;
/// The number of times one command should be repeated.
pub type RepeatCount = usize;
/// Commands
#[derive(Debug, Clone, PartialEq)]
pub enum Cmd {
/// abort
Abort, // Miscellaneous Command
/// accept-line
AcceptLine,
/// beginning-of-history
BeginningOfHistory,
/// capitalize-word
CapitalizeWord,
/// clear-screen
ClearScreen,
/// complete
Complete,
/// downcase-word
DowncaseWord,
/// vi-eof-maybe
EndOfFile,
/// end-of-history
EndOfHistory,
/// forward-search-history
ForwardSearchHistory,
/// history-search-backward
HistorySearchBackward,
/// history-search-forward
HistorySearchForward,
Insert(RepeatCount, String),
Interrupt,
/// backward-delete-char, backward-kill-line, backward-kill-word
/// delete-char, kill-line, kill-word, unix-line-discard, unix-word-rubout,
/// vi-delete, vi-delete-to, vi-rubout
Kill(Movement),
/// backward-char, backward-word, beginning-of-line, end-of-line,
/// forward-char, forward-word, vi-char-search, vi-end-word, vi-next-word,
/// vi-prev-word
Move(Movement),
/// next-history
NextHistory,
Noop,
/// vi-replace
Overwrite(char),
/// previous-history
PreviousHistory,
/// quoted-insert
QuotedInsert,
/// vi-change-char
ReplaceChar(RepeatCount, char),
/// vi-change-to, vi-substitute
Replace(Movement, Option<String>),
/// reverse-search-history
ReverseSearchHistory,
/// self-insert
SelfInsert(RepeatCount, char),
Suspend,
/// transpose-chars
TransposeChars,
/// transpose-words
TransposeWords(RepeatCount),
/// undo
Undo(RepeatCount),
Unknown,
/// upcase-word
UpcaseWord,
/// vi-yank-to
ViYankTo(Movement),
/// yank, vi-put
Yank(RepeatCount, Anchor),
/// yank-pop
YankPop,
}
impl Cmd {
pub fn should_reset_kill_ring(&self) -> bool {
match *self {
Cmd::Kill(Movement::BackwardChar(_)) | Cmd::Kill(Movement::ForwardChar(_)) => true,
Cmd::ClearScreen
| Cmd::Kill(_)
| Cmd::Replace(_, _)
| Cmd::Noop
| Cmd::Suspend
| Cmd::Yank(_, _)
| Cmd::YankPop => false,
_ => true,
}
}
fn is_repeatable_change(&self) -> bool {
match *self {
Cmd::Insert(_, _)
| Cmd::Kill(_)
| Cmd::ReplaceChar(_, _)
| Cmd::Replace(_, _)
| Cmd::SelfInsert(_, _)
| Cmd::ViYankTo(_)
| Cmd::Yank(_, _) => true,
Cmd::TransposeChars => false, // TODO Validate
_ => false,
}
}
fn is_repeatable(&self) -> bool {
match *self {
Cmd::Move(_) => true,
_ => self.is_repeatable_change(),
}
}
// Replay this command with a possible different `RepeatCount`.
fn redo(&self, new: Option<RepeatCount>, wrt: &Refresher) -> Cmd {
match *self {
Cmd::Insert(previous, ref text) => {
Cmd::Insert(repeat_count(previous, new), text.clone())
}
Cmd::Kill(ref mvt) => Cmd::Kill(mvt.redo(new)),
Cmd::Move(ref mvt) => Cmd::Move(mvt.redo(new)),
Cmd::ReplaceChar(previous, c) => Cmd::ReplaceChar(repeat_count(previous, new), c),
Cmd::Replace(ref mvt, ref text) => {
if text.is_none() {
let last_insert = wrt.last_insert();
if let Movement::ForwardChar(0) = mvt {
Cmd::Replace(
Movement::ForwardChar(
last_insert.as_ref().map_or(0, |text| text.len()),
),
last_insert,
)
} else {
Cmd::Replace(mvt.redo(new), last_insert)
}
} else {
Cmd::Replace(mvt.redo(new), text.clone())
}
}
Cmd::SelfInsert(previous, c) => {
// consecutive char inserts are repeatable not only the last one...
if let Some(text) = wrt.last_insert() {
Cmd::Insert(repeat_count(previous, new), text)
} else {
Cmd::SelfInsert(repeat_count(previous, new), c)
}
}
// Cmd::TransposeChars => Cmd::TransposeChars,
Cmd::ViYankTo(ref mvt) => Cmd::ViYankTo(mvt.redo(new)),
Cmd::Yank(previous, anchor) => Cmd::Yank(repeat_count(previous, new), anchor),
_ => unreachable!(),
}
}
}
fn repeat_count(previous: RepeatCount, new: Option<RepeatCount>) -> RepeatCount {
match new {
Some(n) => n,
None => previous,
}
}
/// Different word definitions
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum Word {
/// non-blanks characters
Big,
/// alphanumeric characters
Emacs,
/// alphanumeric (and '_') characters
Vi,
}
/// Where to move with respect to word boundary
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum At {
Start,
BeforeEnd,
AfterEnd,
}
/// Where to paste (relative to cursor position)
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum Anchor {
After,
Before,
}
/// Vi character search
#[derive(Debug, Clone, PartialEq, Copy)]
pub enum CharSearch {
Forward(char),
// until
ForwardBefore(char),
Backward(char),
// until
BackwardAfter(char),
}
impl CharSearch {
fn opposite(self) -> CharSearch {
match self {
CharSearch::Forward(c) => CharSearch::Backward(c),
CharSearch::ForwardBefore(c) => CharSearch::BackwardAfter(c),
CharSearch::Backward(c) => CharSearch::Forward(c),
CharSearch::BackwardAfter(c) => CharSearch::ForwardBefore(c),
}
}
}
/// Where to move
#[derive(Debug, Clone, PartialEq)]
pub enum Movement {
WholeLine, // not really a movement
/// beginning-of-line
BeginningOfLine,
/// end-of-line
EndOfLine,
/// backward-word, vi-prev-word
BackwardWord(RepeatCount, Word), // Backward until start of word
/// forward-word, vi-end-word, vi-next-word
ForwardWord(RepeatCount, At, Word), // Forward until start/end of word
/// vi-char-search
ViCharSearch(RepeatCount, CharSearch),
/// vi-first-print
ViFirstPrint,
/// backward-char
BackwardChar(RepeatCount),
/// forward-char
ForwardChar(RepeatCount),
}
impl Movement {
// Replay this movement with a possible different `RepeatCount`.
fn redo(&self, new: Option<RepeatCount>) -> Movement {
match *self {
Movement::WholeLine => Movement::WholeLine,
Movement::BeginningOfLine => Movement::BeginningOfLine,
Movement::ViFirstPrint => Movement::ViFirstPrint,
Movement::EndOfLine => Movement::EndOfLine,
Movement::BackwardWord(previous, word) => {
Movement::BackwardWord(repeat_count(previous, new), word)
}
Movement::ForwardWord(previous, at, word) => {
Movement::ForwardWord(repeat_count(previous, new), at, word)
}
Movement::ViCharSearch(previous, char_search) => {
Movement::ViCharSearch(repeat_count(previous, new), char_search)
}
Movement::BackwardChar(previous) => Movement::BackwardChar(repeat_count(previous, new)),
Movement::ForwardChar(previous) => Movement::ForwardChar(repeat_count(previous, new)),
}
}
}
#[derive(PartialEq)]
enum InputMode {
/// Vi Command/Alternate
Command,
/// Insert/Input mode
Insert,
/// Overwrite mode
Replace,
}
/// Tranform key(s) to commands based on current input mode
pub struct InputState {
mode: EditMode,
custom_bindings: Arc<RwLock<HashMap<KeyPress, Cmd>>>,
input_mode: InputMode, // vi only ?
// numeric arguments: http://web.mit.edu/gnu/doc/html/rlman_1.html#SEC7
num_args: i16,
last_cmd: Cmd, // vi only
last_char_search: Option<CharSearch>, // vi only
}
pub trait Refresher {
/// Rewrite the currently edited line accordingly to the buffer content,
/// cursor position, and number of columns of the terminal.
fn refresh_line(&mut self) -> Result<()>;
/// Same as `refresh_line` but with a dynamic prompt.
fn refresh_prompt_and_line(&mut self, prompt: &str) -> Result<()>;
/// Vi only, switch to insert mode.
fn doing_insert(&mut self);
/// Vi only, switch to command mode.
fn done_inserting(&mut self);
/// Vi only, last text inserted.
fn last_insert(&self) -> Option<String>;
}
impl InputState {
pub fn new(
config: &Config,
custom_bindings: Arc<RwLock<HashMap<KeyPress, Cmd>>>,
) -> InputState {
InputState {
mode: config.edit_mode(),
custom_bindings,
input_mode: InputMode::Insert,
num_args: 0,
last_cmd: Cmd::Noop,
last_char_search: None,
}
}
pub fn is_emacs_mode(&self) -> bool {
self.mode == EditMode::Emacs
}
/// Parse user input into one command
/// `single_esc_abort` is used in emacs mode on unix platform when a single
/// esc key is expected to abort current action.
pub fn next_cmd<R: RawReader>(
&mut self,
rdr: &mut R,
wrt: &mut Refresher,
single_esc_abort: bool,
) -> Result<Cmd> {
match self.mode {
EditMode::Emacs => self.emacs(rdr, wrt, single_esc_abort),
EditMode::Vi if self.input_mode != InputMode::Command => self.vi_insert(rdr, wrt),
EditMode::Vi => self.vi_command(rdr, wrt),
}
}
// TODO dynamic prompt (arg: ?)
fn emacs_digit_argument<R: RawReader>(
&mut self,
rdr: &mut R,
wrt: &mut Refresher,
digit: char,
) -> Result<KeyPress> {
match digit {
'0'...'9' => {
self.num_args = digit.to_digit(10).unwrap() as i16;
}
'-' => {
self.num_args = -1;
}
_ => unreachable!(),
}
loop {
try!(wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args)));
let key = try!(rdr.next_key(true));
match key {
KeyPress::Char(digit @ '0'...'9') | KeyPress::Meta(digit @ '0'...'9') => {
if self.num_args == -1 {
self.num_args *= digit.to_digit(10).unwrap() as i16;
} else if self.num_args.abs() < 1000 {
// shouldn't ever need more than 4 digits
self.num_args = self
.num_args
.saturating_mul(10)
.saturating_add(digit.to_digit(10).unwrap() as i16);
}
}
KeyPress::Char('-') | KeyPress::Meta('-') => {}
_ => {
try!(wrt.refresh_line());
return Ok(key);
}
};
}
}
fn emacs<R: RawReader>(
&mut self,
rdr: &mut R,
wrt: &mut Refresher,
single_esc_abort: bool,
) -> Result<Cmd> {
let mut key = try!(rdr.next_key(single_esc_abort));
if let KeyPress::Meta(digit @ '-') = key {
key = try!(self.emacs_digit_argument(rdr, wrt, digit));
} else if let KeyPress::Meta(digit @ '0'...'9') = key {
key = try!(self.emacs_digit_argument(rdr, wrt, digit));
}
let (n, positive) = self.emacs_num_args(); // consume them in all cases
{
let bindings = self.custom_bindings.read().unwrap();
if let Some(cmd) = bindings.get(&key) {
debug!(target: "rustyline", "Custom command: {:?}", cmd);
return Ok(if cmd.is_repeatable() {
cmd.redo(Some(n), wrt)
} else {
cmd.clone()
});
}
}
let cmd = match key {
KeyPress::Char(c) => if positive {
Cmd::SelfInsert(n, c)
} else {
Cmd::Unknown
},
KeyPress::Ctrl('A') => Cmd::Move(Movement::BeginningOfLine),
KeyPress::Ctrl('B') => if positive {
Cmd::Move(Movement::BackwardChar(n))
} else {
Cmd::Move(Movement::ForwardChar(n))
},
KeyPress::Ctrl('E') => Cmd::Move(Movement::EndOfLine),
KeyPress::Ctrl('F') => if positive {
Cmd::Move(Movement::ForwardChar(n))
} else {
Cmd::Move(Movement::BackwardChar(n))
},
KeyPress::Ctrl('G') | KeyPress::Esc | KeyPress::Meta('\x07') => Cmd::Abort,
KeyPress::Ctrl('H') | KeyPress::Backspace => if positive {
Cmd::Kill(Movement::BackwardChar(n))
} else {
Cmd::Kill(Movement::ForwardChar(n))
},
KeyPress::Tab => Cmd::Complete,
KeyPress::Ctrl('K') => if positive {
Cmd::Kill(Movement::EndOfLine)
} else {
Cmd::Kill(Movement::BeginningOfLine)
},
KeyPress::Ctrl('L') => Cmd::ClearScreen,
KeyPress::Ctrl('N') => Cmd::NextHistory,
KeyPress::Ctrl('P') => Cmd::PreviousHistory,
KeyPress::Ctrl('X') => {
let snd_key = try!(rdr.next_key(true));
match snd_key {
KeyPress::Ctrl('G') | KeyPress::Esc => Cmd::Abort,
KeyPress::Ctrl('U') => Cmd::Undo(n),
_ => Cmd::Unknown,
}
}
KeyPress::Meta('\x08') | KeyPress::Meta('\x7f') => if positive {
Cmd::Kill(Movement::BackwardWord(n, Word::Emacs))
} else {
Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs))
},
KeyPress::Meta('<') => Cmd::BeginningOfHistory,
KeyPress::Meta('>') => Cmd::EndOfHistory,
KeyPress::Meta('B') | KeyPress::Meta('b') => if positive {
Cmd::Move(Movement::BackwardWord(n, Word::Emacs))
} else {
Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs))
},
KeyPress::Meta('C') | KeyPress::Meta('c') => Cmd::CapitalizeWord,
KeyPress::Meta('D') | KeyPress::Meta('d') => if positive {
Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs))
} else {
Cmd::Kill(Movement::BackwardWord(n, Word::Emacs))
},
KeyPress::Meta('F') | KeyPress::Meta('f') => if positive {
Cmd::Move(Movement::ForwardWord(n, At::AfterEnd, Word::Emacs))
} else {
Cmd::Move(Movement::BackwardWord(n, Word::Emacs))
},
KeyPress::Meta('L') | KeyPress::Meta('l') => Cmd::DowncaseWord,
KeyPress::Meta('T') | KeyPress::Meta('t') => Cmd::TransposeWords(n),
KeyPress::Meta('U') | KeyPress::Meta('u') => Cmd::UpcaseWord,
KeyPress::Meta('Y') | KeyPress::Meta('y') => Cmd::YankPop,
_ => self.common(key, n, positive),
};
debug!(target: "rustyline", "Emacs command: {:?}", cmd);
Ok(cmd)
}
fn vi_arg_digit<R: RawReader>(
&mut self,
rdr: &mut R,
wrt: &mut Refresher,
digit: char,
) -> Result<KeyPress> {
self.num_args = digit.to_digit(10).unwrap() as i16;
loop {
try!(wrt.refresh_prompt_and_line(&format!("(arg: {}) ", self.num_args)));
let key = try!(rdr.next_key(false));
match key {
KeyPress::Char(digit @ '0'...'9') => {
if self.num_args.abs() < 1000 {
// shouldn't ever need more than 4 digits
self.num_args = self
.num_args
.saturating_mul(10)
.saturating_add(digit.to_digit(10).unwrap() as i16);
}
}
_ => {
try!(wrt.refresh_line());
return Ok(key);
}
};
}
}
fn vi_command<R: RawReader>(&mut self, rdr: &mut R, wrt: &mut Refresher) -> Result<Cmd> {
let mut key = try!(rdr.next_key(false));
if let KeyPress::Char(digit @ '1'...'9') = key {
key = try!(self.vi_arg_digit(rdr, wrt, digit));
}
let no_num_args = self.num_args == 0;
let n = self.vi_num_args(); // consume them in all cases
{
let bindings = self.custom_bindings.read().unwrap();
if let Some(cmd) = bindings.get(&key) {
debug!(target: "rustyline", "Custom command: {:?}", cmd);
return Ok(if cmd.is_repeatable() {
if no_num_args {
cmd.redo(None, wrt)
} else {
cmd.redo(Some(n), wrt)
}
} else {
cmd.clone()
});
}
}
let cmd = match key {
KeyPress::Char('$') |
KeyPress::End => Cmd::Move(Movement::EndOfLine),
KeyPress::Char('.') => { // vi-redo (repeat last command)
if no_num_args {
self.last_cmd.redo(None, wrt)
} else {
self.last_cmd.redo(Some(n), wrt)
}
},
// TODO KeyPress::Char('%') => Cmd::???, Move to the corresponding opening/closing bracket
KeyPress::Char('0') => Cmd::Move(Movement::BeginningOfLine),
KeyPress::Char('^') => Cmd::Move(Movement::ViFirstPrint),
KeyPress::Char('a') => {
// vi-append-mode
self.input_mode = InputMode::Insert;
wrt.doing_insert();
Cmd::Move(Movement::ForwardChar(n))
}
KeyPress::Char('A') => {
// vi-append-eol
self.input_mode = InputMode::Insert;
wrt.doing_insert();
Cmd::Move(Movement::EndOfLine)
}
KeyPress::Char('b') => Cmd::Move(Movement::BackwardWord(n, Word::Vi)), // vi-prev-word
KeyPress::Char('B') => Cmd::Move(Movement::BackwardWord(n, Word::Big)),
KeyPress::Char('c') => {
self.input_mode = InputMode::Insert;
match try!(self.vi_cmd_motion(rdr, wrt, key, n)) {
Some(mvt) => Cmd::Replace(mvt, None),
None => Cmd::Unknown,
}
}
KeyPress::Char('C') => {
self.input_mode = InputMode::Insert;
Cmd::Replace(Movement::EndOfLine, None)
}
KeyPress::Char('d') => {
match try!(self.vi_cmd_motion(rdr, wrt, key, n)) {
Some(mvt) => Cmd::Kill(mvt),
None => Cmd::Unknown,
}
}
KeyPress::Char('D') |
KeyPress::Ctrl('K') => Cmd::Kill(Movement::EndOfLine),
KeyPress::Char('e') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Vi)),
KeyPress::Char('E') => Cmd::Move(Movement::ForwardWord(n, At::BeforeEnd, Word::Big)),
KeyPress::Char('i') => {
// vi-insertion-mode
self.input_mode = InputMode::Insert;
wrt.doing_insert();
Cmd::Noop
}
KeyPress::Char('I') => {
// vi-insert-beg
self.input_mode = InputMode::Insert;
wrt.doing_insert();
Cmd::Move(Movement::BeginningOfLine)
}
KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
// vi-char-search
let cs = try!(self.vi_char_search(rdr, c));
match cs {
Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)),
None => Cmd::Unknown,
}
}
KeyPress::Char(';') => {
match self.last_char_search {
Some(cs) => Cmd::Move(Movement::ViCharSearch(n, cs)),
None => Cmd::Noop,
}
}
KeyPress::Char(',') => {
match self.last_char_search {
Some(ref cs) => Cmd::Move(Movement::ViCharSearch(n, cs.opposite())),
None => Cmd::Noop,
}
}
// TODO KeyPress::Char('G') => Cmd::???, Move to the history line n
KeyPress::Char('p') => Cmd::Yank(n, Anchor::After), // vi-put
KeyPress::Char('P') => Cmd::Yank(n, Anchor::Before), // vi-put
KeyPress::Char('r') => {
// vi-replace-char:
let ch = try!(rdr.next_key(false));
match ch {
KeyPress::Char(c) => Cmd::ReplaceChar(n, c),
KeyPress::Esc => Cmd::Noop,
_ => Cmd::Unknown,
}
}
KeyPress::Char('R') => {
// vi-replace-mode (overwrite-mode)
self.input_mode = InputMode::Replace;
Cmd::Replace(Movement::ForwardChar(0), None)
}
KeyPress::Char('s') => {
// vi-substitute-char:
self.input_mode = InputMode::Insert;
Cmd::Replace(Movement::ForwardChar(n), None)
}
KeyPress::Char('S') => {
// vi-substitute-line:
self.input_mode = InputMode::Insert;
Cmd::Replace(Movement::WholeLine, None)
}
KeyPress::Char('u') => Cmd::Undo(n),
// KeyPress::Char('U') => Cmd::???, // revert-line
KeyPress::Char('w') => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Vi)), // vi-next-word
KeyPress::Char('W') => Cmd::Move(Movement::ForwardWord(n, At::Start, Word::Big)), // vi-next-word
KeyPress::Char('x') => Cmd::Kill(Movement::ForwardChar(n)), // vi-delete: TODO move backward if eol
KeyPress::Char('X') => Cmd::Kill(Movement::BackwardChar(n)), // vi-rubout
KeyPress::Char('y') => {
match try!(self.vi_cmd_motion(rdr, wrt, key, n)) {
Some(mvt) => Cmd::ViYankTo(mvt),
None => Cmd::Unknown,
}
}
// KeyPress::Char('Y') => Cmd::???, // vi-yank-to
KeyPress::Char('h') |
KeyPress::Ctrl('H') |
KeyPress::Backspace => Cmd::Move(Movement::BackwardChar(n)),
KeyPress::Ctrl('G') => Cmd::Abort,
KeyPress::Char('l') |
KeyPress::Char(' ') => Cmd::Move(Movement::ForwardChar(n)),
KeyPress::Ctrl('L') => Cmd::ClearScreen,
KeyPress::Char('+') |
KeyPress::Char('j') | // TODO: move to the start of the line.
KeyPress::Ctrl('N') => Cmd::NextHistory,
KeyPress::Char('-') |
KeyPress::Char('k') | // TODO: move to the start of the line.
KeyPress::Ctrl('P') => Cmd::PreviousHistory,
KeyPress::Ctrl('R') => {
self.input_mode = InputMode::Insert; // TODO Validate
Cmd::ReverseSearchHistory
}
KeyPress::Ctrl('S') => {
self.input_mode = InputMode::Insert; // TODO Validate
Cmd::ForwardSearchHistory
}
KeyPress::Esc => Cmd::Noop,
_ => self.common(key, n, true),
};
debug!(target: "rustyline", "Vi command: {:?}", cmd);
if cmd.is_repeatable_change() {
self.last_cmd = cmd.clone();
}
Ok(cmd)
}
fn vi_insert<R: RawReader>(&mut self, rdr: &mut R, wrt: &mut Refresher) -> Result<Cmd> {
let key = try!(rdr.next_key(false));
{
let bindings = self.custom_bindings.read().unwrap();
if let Some(cmd) = bindings.get(&key) {
debug!(target: "rustyline", "Custom command: {:?}", cmd);
return Ok(if cmd.is_repeatable() {
cmd.redo(None, wrt)
} else {
cmd.clone()
});
}
}
let cmd = match key {
KeyPress::Char(c) => if self.input_mode == InputMode::Replace {
Cmd::Overwrite(c)
} else {
Cmd::SelfInsert(1, c)
},
KeyPress::Ctrl('H') | KeyPress::Backspace => Cmd::Kill(Movement::BackwardChar(1)),
KeyPress::Tab => Cmd::Complete,
KeyPress::Esc => {
// vi-movement-mode/vi-command-mode
self.input_mode = InputMode::Command;
wrt.done_inserting();
Cmd::Move(Movement::BackwardChar(1))
}
_ => self.common(key, 1, true),
};
debug!(target: "rustyline", "Vi insert: {:?}", cmd);
if cmd.is_repeatable_change() {
if let (Cmd::Replace(_, _), Cmd::SelfInsert(_, _)) = (&self.last_cmd, &cmd) {
// replacing...
} else if let (Cmd::SelfInsert(_, _), Cmd::SelfInsert(_, _)) = (&self.last_cmd, &cmd) {
// inserting...
} else {
self.last_cmd = cmd.clone();
}
}
Ok(cmd)
}
fn vi_cmd_motion<R: RawReader>(
&mut self,
rdr: &mut R,
wrt: &mut Refresher,
key: KeyPress,
n: RepeatCount,
) -> Result<Option<Movement>> {
let mut mvt = try!(rdr.next_key(false));
if mvt == key {
return Ok(Some(Movement::WholeLine));
}
let mut n = n;
if let KeyPress::Char(digit @ '1'...'9') = mvt {
// vi-arg-digit
mvt = try!(self.vi_arg_digit(rdr, wrt, digit));
n = self.vi_num_args().saturating_mul(n);
}
Ok(match mvt {
KeyPress::Char('$') => Some(Movement::EndOfLine),
KeyPress::Char('0') => Some(Movement::BeginningOfLine),
KeyPress::Char('^') => Some(Movement::ViFirstPrint),
KeyPress::Char('b') => Some(Movement::BackwardWord(n, Word::Vi)),
KeyPress::Char('B') => Some(Movement::BackwardWord(n, Word::Big)),
KeyPress::Char('e') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi)),
KeyPress::Char('E') => Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big)),
KeyPress::Char(c) if c == 'f' || c == 'F' || c == 't' || c == 'T' => {
let cs = try!(self.vi_char_search(rdr, c));
match cs {
Some(cs) => Some(Movement::ViCharSearch(n, cs)),
None => None,
}
}
KeyPress::Char(';') => match self.last_char_search {
Some(cs) => Some(Movement::ViCharSearch(n, cs)),
None => None,
},
KeyPress::Char(',') => match self.last_char_search {
Some(ref cs) => Some(Movement::ViCharSearch(n, cs.opposite())),
None => None,
},
KeyPress::Char('h') | KeyPress::Ctrl('H') | KeyPress::Backspace => {
Some(Movement::BackwardChar(n))
}
KeyPress::Char('l') | KeyPress::Char(' ') => Some(Movement::ForwardChar(n)),
KeyPress::Char('w') => {
// 'cw' is 'ce'
if key == KeyPress::Char('c') {
Some(Movement::ForwardWord(n, At::AfterEnd, Word::Vi))
} else {
Some(Movement::ForwardWord(n, At::Start, Word::Vi))
}
}
KeyPress::Char('W') => {
// 'cW' is 'cE'
if key == KeyPress::Char('c') {
Some(Movement::ForwardWord(n, At::AfterEnd, Word::Big))
} else {
Some(Movement::ForwardWord(n, At::Start, Word::Big))
}
}
_ => None,
})
}
fn vi_char_search<R: RawReader>(
&mut self,
rdr: &mut R,
cmd: char,
) -> Result<Option<CharSearch>> {
let ch = try!(rdr.next_key(false));
Ok(match ch {
KeyPress::Char(ch) => {
let cs = match cmd {
'f' => CharSearch::Forward(ch),
't' => CharSearch::ForwardBefore(ch),
'F' => CharSearch::Backward(ch),
'T' => CharSearch::BackwardAfter(ch),
_ => unreachable!(),
};
self.last_char_search = Some(cs);
Some(cs)
}
_ => None,
})
}
fn common(&mut self, key: KeyPress, n: RepeatCount, positive: bool) -> Cmd {
match key {
KeyPress::Home => Cmd::Move(Movement::BeginningOfLine),
KeyPress::Left => {
if positive {
Cmd::Move(Movement::BackwardChar(n))
} else {
Cmd::Move(Movement::ForwardChar(n))
}
}
KeyPress::Ctrl('C') => Cmd::Interrupt,
KeyPress::Ctrl('D') => Cmd::EndOfFile,
KeyPress::Delete => {
if positive {
Cmd::Kill(Movement::ForwardChar(n))
} else {
Cmd::Kill(Movement::BackwardChar(n))
}
}
KeyPress::End => Cmd::Move(Movement::EndOfLine),
KeyPress::Right => {
if positive {
Cmd::Move(Movement::ForwardChar(n))
} else {
Cmd::Move(Movement::BackwardChar(n))
}
}
KeyPress::Ctrl('J') |
KeyPress::Enter => Cmd::AcceptLine,
KeyPress::Down => Cmd::NextHistory,
KeyPress::Up => Cmd::PreviousHistory,
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') => {
if positive {
Cmd::Kill(Movement::BeginningOfLine)
} else {
Cmd::Kill(Movement::EndOfLine)
}
},
KeyPress::Ctrl('Q') | // most terminals override Ctrl+Q to resume execution
KeyPress::Ctrl('V') => Cmd::QuotedInsert,
KeyPress::Ctrl('W') => {
if positive {
Cmd::Kill(Movement::BackwardWord(n, Word::Big))
} else {
Cmd::Kill(Movement::ForwardWord(n, At::AfterEnd, Word::Big))
}
}
KeyPress::Ctrl('Y') => {
if positive {
Cmd::Yank(n, Anchor::Before)
} else {
Cmd::Unknown // TODO Validate
}
}
KeyPress::Ctrl('Z') => Cmd::Suspend,
KeyPress::Ctrl('_') => Cmd::Undo(n),
KeyPress::UnknownEscSeq => Cmd::Noop,
_ => Cmd::Unknown,
}
}
fn num_args(&mut self) -> i16 {
let num_args = match self.num_args {
0 => 1,
_ => self.num_args,
};
self.num_args = 0;
num_args
}
fn emacs_num_args(&mut self) -> (RepeatCount, bool) {
let num_args = self.num_args();
if num_args < 0 {
if let (n, false) = num_args.overflowing_abs() {
(n as RepeatCount, false)
} else {
(RepeatCount::max_value(), false)
}
} else {
(num_args as RepeatCount, true)
}
}
fn vi_num_args(&mut self) -> RepeatCount {
let num_args = self.num_args();
if num_args < 0 {
unreachable!()
} else {
num_args.abs() as RepeatCount
}
}
}