diff --git a/Cargo.toml b/Cargo.toml index 643422d74831de6bddfa8b9bc83c10374f735116..7ef2aa4f03f4a199ce20431fe878302fda18fb19 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,3 +28,4 @@ winapi = { version = "0.3", features = ["consoleapi", "handleapi", "minwindef", [dev-dependencies] tempdir = "0.3" +assert_matches = "1.2" diff --git a/src/edit.rs b/src/edit.rs index 045ed3a062d5aa498360ab69404ed1d59d26b593..0cce2b66ea13ccabb2af2e7128ad949038891544 100644 --- a/src/edit.rs +++ b/src/edit.rs @@ -493,10 +493,11 @@ pub fn init_state<'out>(out: &'out mut Renderer, line: &str, pos: usize) -> Stat mod test { use super::init_state; use history::History; + use tty::Sink; #[test] fn edit_history_next() { - let mut out = ::std::io::sink(); + let mut out = Sink::new(); let line = "current edited line"; let mut s = init_state(&mut out, line, 6); let mut history = History::new(); diff --git a/src/keymap.rs b/src/keymap.rs index 9656e3ac9ed2fb797fa660eeef1d3256c26a54ff..e08838fc51ec93d1603123b48f8fe9e037f30658 100644 --- a/src/keymap.rs +++ b/src/keymap.rs @@ -813,9 +813,9 @@ impl InputState { KeyPress::Ctrl('T') => Cmd::TransposeChars, KeyPress::Ctrl('U') => { if positive { - Cmd::Kill(Movement::BeginningOfLine) + Cmd::Kill(Movement::BeginningOfLine) } else { - Cmd::Kill(Movement::EndOfLine) + Cmd::Kill(Movement::EndOfLine) } }, KeyPress::Ctrl('Q') | // most terminals override Ctrl+Q to resume execution diff --git a/src/lib.rs b/src/lib.rs index e898b343c3cbd39ade3ed683070c71e247dbe980..903fbaa88e3bd901ddfd77de7a4866173278cdda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -802,122 +802,7 @@ impl<'a, H: Helper> Iterator for Iter<'a, H> { } #[cfg(test)] -mod test { - use std::cell::RefCell; - use std::collections::HashMap; - use std::rc::Rc; - - use super::{Editor, Result}; - use completion::Completer; - use config::Config; - use consts::KeyPress; - use edit::init_state; - use keymap::{Cmd, InputState}; - - fn init_editor(keys: &[KeyPress]) -> Editor<()> { - let mut editor = Editor::<()>::new(); - editor.term.keys.extend(keys.iter().cloned()); - editor - } - - struct SimpleCompleter; - impl Completer for SimpleCompleter { - fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> { - Ok((0, vec![line.to_owned() + "t"])) - } - } - - #[test] - fn complete_line() { - let mut out = ::std::io::sink(); - let mut s = init_state(&mut out, "rus", 3); - let config = Config::default(); - let mut input_state = InputState::new(&config, Rc::new(RefCell::new(HashMap::new()))); - let keys = &[KeyPress::Enter]; - let mut rdr = keys.iter(); - let completer = SimpleCompleter; - let cmd = super::complete_line( - &mut rdr, - &mut s, - &mut input_state, - &completer, - &Config::default(), - ).unwrap(); - assert_eq!(Some(Cmd::AcceptLine), cmd); - assert_eq!("rust", s.line.as_str()); - assert_eq!(4, s.line.pos()); - } - - fn assert_line(keys: &[KeyPress], expected_line: &str) { - let mut editor = init_editor(keys); - let actual_line = editor.readline(&">>").unwrap(); - assert_eq!(expected_line, actual_line); - } - - #[test] - fn delete_key() { - assert_line( - &[KeyPress::Char('a'), KeyPress::Delete, KeyPress::Enter], - "a", - ); - assert_line( - &[ - KeyPress::Char('a'), - KeyPress::Left, - KeyPress::Delete, - KeyPress::Enter, - ], - "", - ); - } - - #[test] - fn down_key() { - assert_line(&[KeyPress::Down, KeyPress::Enter], ""); - } - - #[test] - fn end_key() { - assert_line(&[KeyPress::End, KeyPress::Enter], ""); - } - - #[test] - fn home_key() { - assert_line(&[KeyPress::Home, KeyPress::Enter], ""); - } - - #[test] - fn left_key() { - assert_line(&[KeyPress::Left, KeyPress::Enter], ""); - } - - #[test] - fn meta_backspace_key() { - assert_line(&[KeyPress::Meta('\x08'), KeyPress::Enter], ""); - } - - #[test] - fn page_down_key() { - assert_line(&[KeyPress::PageDown, KeyPress::Enter], ""); - } - - #[test] - fn page_up_key() { - assert_line(&[KeyPress::PageUp, KeyPress::Enter], ""); - } - - #[test] - fn right_key() { - assert_line(&[KeyPress::Right, KeyPress::Enter], ""); - } - - #[test] - fn up_key() { - assert_line(&[KeyPress::Up, KeyPress::Enter], ""); - } - - #[test] - fn unknown_esc_key() { - assert_line(&[KeyPress::UnknownEscSeq, KeyPress::Enter], ""); - } -} +#[macro_use] +extern crate assert_matches; +#[cfg(test)] +mod test; diff --git a/src/test/common.rs b/src/test/common.rs new file mode 100644 index 0000000000000000000000000000000000000000..8205bc2d857197b5f21be11c7c9a355b6e80168b --- /dev/null +++ b/src/test/common.rs @@ -0,0 +1,112 @@ +use super::{assert_cursor, assert_line, assert_line_with_initial, init_editor}; +use consts::KeyPress; +use error::ReadlineError; + +#[test] +fn home_key() { + assert_cursor(("", ""), &[KeyPress::Home, KeyPress::Enter], 0); + assert_cursor(("Hi", ""), &[KeyPress::Home, KeyPress::Enter], 0); +} + +#[test] +fn end_key() { + assert_cursor(("", ""), &[KeyPress::End, KeyPress::Enter], 0); + //assert_cursor(("H", "i"), &[KeyPress::End, KeyPress::Enter], 2); FIXME +} + +#[test] +fn left_key() { + assert_cursor(("Hi", ""), &[KeyPress::Left, KeyPress::Enter], 1); + assert_cursor(("H", "i"), &[KeyPress::Left, KeyPress::Enter], 0); + assert_cursor(("", "Hi"), &[KeyPress::Left, KeyPress::Enter], 0); +} + +#[test] +fn right_key() { + assert_cursor(("", ""), &[KeyPress::Right, KeyPress::Enter], 0); + assert_cursor(("", "Hi"), &[KeyPress::Right, KeyPress::Enter], 1); + assert_cursor(("B", "ye"), &[KeyPress::Right, KeyPress::Enter], 2); +} + +#[test] +fn enter_key() { + assert_line(&[KeyPress::Enter], ""); + assert_line(&[KeyPress::Char('a'), KeyPress::Enter], "a"); + assert_line_with_initial(("Hi", ""), &[KeyPress::Enter], "Hi"); + assert_line_with_initial(("", "Hi"), &[KeyPress::Enter], "Hi"); + assert_line_with_initial(("H", "i"), &[KeyPress::Enter], "Hi"); +} + +#[test] +fn newline_key() { + assert_line(&[KeyPress::Ctrl('J')], ""); + assert_line(&[KeyPress::Char('a'), KeyPress::Ctrl('J')], "a"); +} + +#[test] +fn eof_key() { + let mut editor = init_editor(&[KeyPress::Ctrl('D')]); + let err = editor.readline(">>"); + assert_matches!(err, Err(ReadlineError::Eof)); + + assert_line( + &[KeyPress::Char('a'), KeyPress::Ctrl('D'), KeyPress::Enter], + "a", + ); + assert_line_with_initial(("", "Hi"), &[KeyPress::Ctrl('D'), KeyPress::Enter], "i"); +} + +#[test] +fn interrupt_key() { + let mut editor = init_editor(&[KeyPress::Ctrl('C')]); + let err = editor.readline(">>"); + assert_matches!(err, Err(ReadlineError::Interrupted)); + + let mut editor = init_editor(&[KeyPress::Ctrl('C')]); + let err = editor.readline_with_initial(">>", ("Hi", "")); + assert_matches!(err, Err(ReadlineError::Interrupted)); +} + +#[test] +fn delete_key() { + assert_line_with_initial(("a", ""), &[KeyPress::Delete, KeyPress::Enter], "a"); + assert_line_with_initial(("", "a"), &[KeyPress::Delete, KeyPress::Enter], ""); +} + +#[test] +fn ctrl_t() { + assert_line_with_initial(("a", "b"), &[KeyPress::Ctrl('T'), KeyPress::Enter], "ba"); + assert_line_with_initial( + ("ab", "cd"), + &[KeyPress::Ctrl('T'), KeyPress::Enter], + "acbd", + ); +} + +#[test] +fn ctrl_u() { + assert_line_with_initial(("a", "b"), &[KeyPress::Ctrl('U'), KeyPress::Enter], "b"); + assert_line_with_initial(("", "a"), &[KeyPress::Ctrl('U'), KeyPress::Enter], "a"); +} + +#[test] +fn ctrl_v() { + assert_line( + &[KeyPress::Ctrl('V'), KeyPress::Char('\t'), KeyPress::Enter], + "\t", + ); +} + +#[test] +fn ctrl_w() { + assert_line_with_initial( + ("Hello, ", "world"), + &[KeyPress::Ctrl('W'), KeyPress::Enter], + "world", + ); + assert_line_with_initial( + ("Hello, world.", ""), + &[KeyPress::Ctrl('W'), KeyPress::Enter], + "Hello, ", + ); +} diff --git a/src/test/mod.rs b/src/test/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..b74186834f15bfd447fe2450e5e4212d6bc275ea --- /dev/null +++ b/src/test/mod.rs @@ -0,0 +1,93 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +use super::{Editor, Result}; +use completion::Completer; +use config::Config; +use consts::KeyPress; +use edit::init_state; +use keymap::{Cmd, InputState}; +use tty::Sink; + +mod common; + +fn init_editor(keys: &[KeyPress]) -> Editor<()> { + let mut editor = Editor::<()>::new(); + editor.term.keys.extend(keys.iter().cloned()); + editor +} + +struct SimpleCompleter; +impl Completer for SimpleCompleter { + fn complete(&self, line: &str, _pos: usize) -> Result<(usize, Vec<String>)> { + Ok((0, vec![line.to_owned() + "t"])) + } +} + +#[test] +fn complete_line() { + let mut out = Sink::new(); + let mut s = init_state(&mut out, "rus", 3); + let config = Config::default(); + let mut input_state = InputState::new(&config, Rc::new(RefCell::new(HashMap::new()))); + let keys = &[KeyPress::Enter]; + let mut rdr = keys.iter(); + let completer = SimpleCompleter; + let cmd = super::complete_line( + &mut rdr, + &mut s, + &mut input_state, + &completer, + &Config::default(), + ).unwrap(); + assert_eq!(Some(Cmd::AcceptLine), cmd); + assert_eq!("rust", s.line.as_str()); + assert_eq!(4, s.line.pos()); +} + +fn assert_line(keys: &[KeyPress], expected_line: &str) { + let mut editor = init_editor(keys); + let actual_line = editor.readline(">>").unwrap(); + assert_eq!(expected_line, actual_line); +} +fn assert_line_with_initial(initial: (&str, &str), keys: &[KeyPress], expected_line: &str) { + let mut editor = init_editor(keys); + let actual_line = editor.readline_with_initial(">>", initial).unwrap(); + assert_eq!(expected_line, actual_line); +} +fn assert_cursor(initial: (&str, &str), keys: &[KeyPress], expected_cursor: usize) { + let mut editor = init_editor(keys); + editor.readline_with_initial("", initial).unwrap(); + assert_eq!(expected_cursor, editor.term.cursor.get()); +} + +#[test] +fn down_key() { + assert_line(&[KeyPress::Down, KeyPress::Enter], ""); +} + +#[test] +fn meta_backspace_key() { + assert_line(&[KeyPress::Meta('\x08'), KeyPress::Enter], ""); +} + +#[test] +fn page_down_key() { + assert_line(&[KeyPress::PageDown, KeyPress::Enter], ""); +} + +#[test] +fn page_up_key() { + assert_line(&[KeyPress::PageUp, KeyPress::Enter], ""); +} + +#[test] +fn up_key() { + assert_line(&[KeyPress::Up, KeyPress::Enter], ""); +} + +#[test] +fn unknown_esc_key() { + assert_line(&[KeyPress::UnknownEscSeq, KeyPress::Enter], ""); +} diff --git a/src/tty/test.rs b/src/tty/test.rs index 7a19c9ac074e1ae1ab354f33ed0f2610b9c02d55..3d7933f64e3e93d46289c874602b239916e3239f 100644 --- a/src/tty/test.rs +++ b/src/tty/test.rs @@ -1,6 +1,7 @@ //! Tests specific definitions -use std::io::{self, Sink, Write}; +use std::cell::Cell; use std::iter::IntoIterator; +use std::rc::Rc; use std::slice::Iter; use std::vec::IntoIter; @@ -41,40 +42,60 @@ impl RawReader for IntoIter<KeyPress> { } #[cfg(unix)] fn next_char(&mut self) -> Result<char> { - unimplemented!(); + match self.next() { + Some(KeyPress::Char(c)) => Ok(c), + None => Err(ReadlineError::Eof), + _ => unimplemented!(), + } + } +} + +pub struct Sink { + cursor: Rc<Cell<usize>>, // cursor position before last command + last: usize, +} + +impl Sink { + pub fn new() -> Sink { + Sink { + cursor: Rc::new(Cell::new(0)), + last: 0, + } } } impl Renderer for Sink { - fn move_cursor(&mut self, _: Position, _: Position) -> Result<()> { + fn move_cursor(&mut self, _: Position, new: Position) -> Result<()> { + self.cursor.replace(self.last); + self.last = new.col; Ok(()) } fn refresh_line( &mut self, - prompt: &str, + _: &str, prompt_size: Position, line: &LineBuffer, hint: Option<String>, _: usize, _: usize, ) -> Result<(Position, Position)> { - try!(self.write_all(prompt.as_bytes())); - try!(self.write_all(line.as_bytes())); + let cursor = self.calculate_position(&line[..line.pos()], prompt_size); + self.last = cursor.col; if let Some(hint) = hint { - try!(self.write_all(truncate(&hint, 0, 80).as_bytes())); + truncate(&hint, 0, 80); } - Ok((prompt_size, prompt_size)) + let end = self.calculate_position(&line, prompt_size); + Ok((cursor, end)) } - /// Characters with 2 column width are correctly handled (not splitted). - fn calculate_position(&self, _: &str, orig: Position) -> Position { - orig + fn calculate_position(&self, s: &str, orig: Position) -> Position { + let mut pos = orig; + pos.col += s.len(); + pos } - fn write_and_flush(&mut self, buf: &[u8]) -> Result<()> { - try!(self.write_all(buf)); - try!(self.flush()); + fn write_and_flush(&mut self, _: &[u8]) -> Result<()> { Ok(()) } @@ -82,12 +103,10 @@ impl Renderer for Sink { Ok(()) } - /// Clear the screen. Used to handle ctrl+l fn clear_screen(&mut self) -> Result<()> { Ok(()) } - /// Check if a SIGWINCH signal has been received fn sigwinch(&self) -> bool { false } @@ -105,6 +124,7 @@ pub type Terminal = DummyTerminal; #[derive(Clone, Debug)] pub struct DummyTerminal { pub keys: Vec<KeyPress>, + pub cursor: Rc<Cell<usize>>, // cursor position before last command } impl Term for DummyTerminal { @@ -113,18 +133,18 @@ impl Term for DummyTerminal { type Mode = Mode; fn new() -> DummyTerminal { - DummyTerminal { keys: Vec::new() } + DummyTerminal { + keys: Vec::new(), + cursor: Rc::new(Cell::new(0)), + } } // Init checks: - /// Check if current terminal can provide a rich line-editing user - /// interface. fn is_unsupported(&self) -> bool { false } - /// check if stdin is connected to a terminal. fn is_stdin_tty(&self) -> bool { true } @@ -135,13 +155,15 @@ impl Term for DummyTerminal { Ok(()) } - /// Create a RAW reader fn create_reader(&self, _: &Config) -> Result<IntoIter<KeyPress>> { Ok(self.keys.clone().into_iter()) } fn create_writer(&self) -> Sink { - io::sink() + Sink { + cursor: self.cursor.clone(), + last: 0, + } } }