Commit 4ee83a2d authored by Liam's avatar Liam Committed by GitHub
Browse files

Merge pull request #26 from iamcodemaker/refactor

add KeyMap for vi
parents 875aefef 249410a0
......@@ -19,5 +19,5 @@ name = "liner"
name = "liner_test"
[dependencies]
termion = "1.0"
termion = "1.1.2"
unicode-width = "0.1.*"
extern crate liner;
use liner::Context;
use liner::KeyBindings;
fn main() {
let mut con = Context::new();
......@@ -12,6 +13,18 @@ fn main() {
break;
}
match res.as_str() {
"emacs" => {
con.key_bindings = KeyBindings::Emacs;
println!("emacs mode");
}
"vi" => {
con.key_bindings = KeyBindings::Vi;
println!("vi mode");
}
_ => {}
}
con.history.push(res.into()).unwrap();
}
}
......@@ -39,10 +39,18 @@ pub fn get_buffer_words(buf: &Buffer) -> Vec<(usize, usize)> {
res
}
/// The key bindings to use.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyBindings {
Vi,
Emacs,
}
pub struct Context {
pub history: History,
pub completer: Option<Box<Completer>>,
pub word_fn: Box<Fn(&Buffer) -> Vec<(usize, usize)>>,
pub key_bindings: KeyBindings,
}
impl Context {
......@@ -51,6 +59,7 @@ impl Context {
history: History::new(),
completer: None,
word_fn: Box::new(get_buffer_words),
key_bindings: KeyBindings::Emacs,
}
}
......@@ -65,7 +74,10 @@ impl Context {
let res = {
let stdout = stdout().into_raw_mode().unwrap();
let ed = try!(Editor::new(stdout, prompt.into(), self));
Self::handle_keys(keymap::Emacs::new(ed), handler)
match self.key_bindings {
KeyBindings::Emacs => Self::handle_keys(keymap::Emacs::new(ed), handler),
KeyBindings::Vi => Self::handle_keys(keymap::Vi::new(ed), handler),
}
};
self.revert_all_history();
......
......@@ -80,6 +80,10 @@ pub struct Editor<'a, W: Write> {
// If this is true, on the next tab we print the completion list.
show_completions_hint: bool,
// if set, the cursor will not be allow to move one past the end of the line, this is necessary
// for Vi's normal mode.
pub no_eol: bool,
}
macro_rules! cur_buf_mut {
......@@ -123,6 +127,7 @@ impl<'a, W: Write> Editor<'a, W> {
show_completions_hint: false,
prompt_width: prompt_width,
term_cursor_line: 1,
no_eol: false,
};
try!(ed.print_current_buffer(true));
......@@ -531,9 +536,16 @@ impl<'a, W: Write> Editor<'a, W> {
}
}
// can't move past the last character in vi normal mode
if self.no_eol {
if self.cursor >= 1 && self.cursor == buf_num_chars {
self.cursor -= 1;
}
}
self.term_cursor_line = (self.prompt_width + buf.range_width(0, self.cursor) + w) / w;
if !move_cursor_to_end_of_line {
if !move_cursor_to_end_of_line || self.no_eol {
// The term cursor is now on the bottom line. We may need to move the term cursor up
// to the line where the true cursor is.
let cursor_line_diff = new_num_lines as isize - self.term_cursor_line as isize;
......
......@@ -32,5 +32,8 @@ pub trait KeyMap<'a, W: Write, T>: From<T> {
}
}
pub mod vi;
pub use vi::Vi;
pub mod emacs;
pub use emacs::Emacs;
use std::io::{self, Write};
use termion::event::Key;
use KeyMap;
use Editor;
/// The editing mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mode {
Insert,
Normal,
}
struct ModeStack(Vec<Mode>);
impl ModeStack {
fn with_insert() -> Self {
ModeStack(vec![Mode::Insert])
}
/// Get the current mode.
///
/// If the stack is empty, we are in normal mode.
fn mode(&self) -> Mode {
self.0.last()
.map(|&m| m)
.unwrap_or(Mode::Normal)
}
/// Push the given mode on to the stack.
fn push(&mut self, m: Mode) {
self.0.push(m)
}
fn pop(&mut self) -> Mode {
self.0.pop()
.unwrap_or(Mode::Normal)
}
}
pub struct Vi<'a, W: Write> {
ed: Editor<'a, W>,
mode_stack: ModeStack,
}
impl<'a, W: Write> Vi<'a, W> {
pub fn new(ed: Editor<'a, W>) -> Self {
Vi {
ed: ed,
mode_stack: ModeStack::with_insert(),
}
}
/// Get the current mode.
fn mode(&self) -> Mode {
self.mode_stack.mode()
}
fn set_mode(&mut self, mode: Mode) {
self.ed.no_eol = mode == Mode::Normal;
self.mode_stack.push(mode);
}
fn pop_mode(&mut self) {
self.mode_stack.pop();
self.ed.no_eol = self.mode() == Mode::Normal;
}
fn handle_key_common(&mut self, key: Key) -> io::Result<()> {
match key {
Key::Ctrl('l') => self.ed.clear(),
Key::Left => self.ed.move_cursor_left(1),
Key::Right => self.ed.move_cursor_right(1),
Key::Up => self.ed.move_up(),
Key::Down => self.ed.move_down(),
Key::Home => self.ed.move_cursor_to_start_of_line(),
Key::End => self.ed.move_cursor_to_end_of_line(),
Key::Backspace => self.ed.delete_before_cursor(),
Key::Delete => self.ed.delete_after_cursor(),
Key::Null => Ok(()),
_ => Ok(()),
}
}
fn handle_key_insert(&mut self, key: Key) -> io::Result<()> {
match key {
Key::Esc => {
// cursor moves to the left when switching from insert to normal mode
try!(self.ed.move_cursor_left(1));
self.pop_mode();
Ok(())
}
Key::Char(c) => self.ed.insert_after_cursor(c),
_ => self.handle_key_common(key),
}
}
fn handle_key_normal(&mut self, key: Key) -> io::Result<()> {
use self::Mode::*;
match key {
Key::Char('i') => {
self.set_mode(Insert);
Ok(())
}
Key::Char('a') => {
self.set_mode(Insert);
self.ed.move_cursor_right(1)
}
Key::Char('A') => {
self.set_mode(Insert);
self.ed.move_cursor_to_end_of_line()
}
Key::Char('I') => {
self.set_mode(Insert);
self.ed.move_cursor_to_start_of_line()
}
Key::Char('h') | Key::Left | Key::Backspace => {
self.ed.move_cursor_left(1)
}
Key::Char('l') | Key::Right | Key::Char(' ') => {
self.ed.move_cursor_right(1)
}
Key::Char('k') => self.ed.move_up(),
Key::Char('j') => self.ed.move_down(),
Key::Char('0') => self.ed.move_cursor_to_start_of_line(),
Key::Char('$') => self.ed.move_cursor_to_end_of_line(),
_ => self.handle_key_common(key),
}
}
}
impl<'a, W: Write> KeyMap<'a, W, Vi<'a, W>> for Vi<'a, W> {
fn handle_key_core(&mut self, key: Key) -> io::Result<()> {
match self.mode() {
Mode::Normal => self.handle_key_normal(key),
Mode::Insert => self.handle_key_insert(key),
}
}
fn editor(&mut self) -> &mut Editor<'a, W> {
&mut self.ed
}
}
impl<'a, W: Write> From<Vi<'a, W>> for String {
fn from(vi: Vi<'a, W>) -> String {
vi.ed.into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use termion::event::Key;
use termion::event::Key::*;
use Context;
use Editor;
use KeyMap;
use std::io::Write;
macro_rules! simulate_keys {
($keymap:ident, $keys:expr) => {{
simulate_keys(&mut $keymap, $keys.into_iter())
}}
}
fn simulate_keys<'a, 'b, W: Write, T, M: KeyMap<'a, W, T>, I>(keymap: &mut M, keys: I) -> bool
where I: Iterator<Item=&'b Key>
{
for k in keys {
if keymap.handle_key(*k, &mut |_| {}).unwrap() {
return true;
}
}
false
}
#[test]
fn enter_is_done() {
let mut context = Context::new();
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), &mut context).unwrap();
let mut map = Vi::new(ed);
map.ed.insert_str_after_cursor("done").unwrap();
assert_eq!(map.ed.cursor(), 4);
assert!(simulate_keys!(map, [
Char('\n'),
]));
assert_eq!(map.ed.cursor(), 4);
assert_eq!(String::from(map), "done");
}
#[test]
fn move_cursor_left() {
let mut context = Context::new();
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), &mut context).unwrap();
let mut map = Vi::new(ed);
map.editor().insert_str_after_cursor("let").unwrap();
assert_eq!(map.ed.cursor(), 3);
simulate_keys!(map, [
Left,
Char('f'),
]);
assert_eq!(map.ed.cursor(), 3);
assert_eq!(String::from(map), "left");
}
#[test]
fn cursor_movement() {
let mut context = Context::new();
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), &mut context).unwrap();
let mut map = Vi::new(ed);
map.ed.insert_str_after_cursor("right").unwrap();
assert_eq!(map.ed.cursor(), 5);
simulate_keys!(map, [
Left,
Left,
Right,
]);
assert_eq!(map.ed.cursor(), 4);
}
#[test]
fn vi_initial_insert() {
let mut context = Context::new();
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), &mut context).unwrap();
let mut map = Vi::new(ed);
simulate_keys!(map, [
Char('i'),
Char('n'),
Char('s'),
Char('e'),
Char('r'),
Char('t'),
]);
assert_eq!(map.ed.cursor(), 6);
assert_eq!(String::from(map), "insert");
}
#[test]
fn vi_left_right_movement() {
let mut context = Context::new();
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), &mut context).unwrap();
let mut map = Vi::new(ed);
map.ed.insert_str_after_cursor("data").unwrap();
assert_eq!(map.ed.cursor(), 4);
simulate_keys!(map, [Left]);
assert_eq!(map.ed.cursor(), 3);
simulate_keys!(map, [Right]);
assert_eq!(map.ed.cursor(), 4);
// switching from insert mode moves the cursor left
simulate_keys!(map, [Esc, Left]);
assert_eq!(map.ed.cursor(), 2);
simulate_keys!(map, [Right]);
assert_eq!(map.ed.cursor(), 3);
simulate_keys!(map, [Char('h')]);
assert_eq!(map.ed.cursor(), 2);
simulate_keys!(map, [Char('l')]);
assert_eq!(map.ed.cursor(), 3);
}
#[test]
/// Shouldn't be able to move past the last char in vi normal mode
fn vi_no_eol() {
let mut context = Context::new();
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), &mut context).unwrap();
let mut map = Vi::new(ed);
map.ed.insert_str_after_cursor("data").unwrap();
assert_eq!(map.ed.cursor(), 4);
simulate_keys!(map, [Esc]);
assert_eq!(map.ed.cursor(), 3);
simulate_keys!(map, [Right, Right]);
assert_eq!(map.ed.cursor(), 3);
// in insert mode, we can move past the last char, but no further
simulate_keys!(map, [Char('i'), Right, Right]);
assert_eq!(map.ed.cursor(), 4);
}
#[test]
/// Cursor moves left when exiting insert mode.
fn vi_switch_from_insert() {
let mut context = Context::new();
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), &mut context).unwrap();
let mut map = Vi::new(ed);
map.ed.insert_str_after_cursor("data").unwrap();
assert_eq!(map.ed.cursor(), 4);
simulate_keys!(map, [Esc]);
assert_eq!(map.ed.cursor(), 3);
simulate_keys!(map, [
Char('i'),
Esc,
Char('i'),
Esc,
Char('i'),
Esc,
Char('i'),
Esc,
]);
assert_eq!(map.ed.cursor(), 0);
}
#[test]
fn vi_normal_history_cursor_eol() {
let mut context = Context::new();
context.history.push("history".into()).unwrap();
context.history.push("history".into()).unwrap();
let out = Vec::new();
let ed = Editor::new(out, "prompt".to_owned(), &mut context).unwrap();
let mut map = Vi::new(ed);
map.ed.insert_str_after_cursor("data").unwrap();
assert_eq!(map.ed.cursor(), 4);
simulate_keys!(map, [Up]);
assert_eq!(map.ed.cursor(), 7);
// in normal mode, make sure we don't end up past the last char
simulate_keys!(map, [Esc, Up]);
assert_eq!(map.ed.cursor(), 6);
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment